pact-foundation / pact-net

.NET version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
https://pact.io
MIT License
846 stars 233 forks source link
apis contract-testing csharp dotnet pact pact-net smartbear-supported

logo

Pact Net

Build status

Fast, easy and reliable testing for your APIs and microservices.

Pact is the de-facto API contract testing tool. Replace expensive and brittle end-to-end integration tests with fast, reliable and easy to debug unit tests.

Why use Pact?

Contract testing with Pact lets you:

Watch our series on the problems with end-to-end integrated tests, and how contract testing can help.

----------

Documentation

Tutorial (60 minutes)

Learn everything in Pact Net in 60 minutes

Upgrade Guides

Older Versions

Need Help

Installation

Via Nuget

----------

Usage

In the sections below, we provide a brief sample of the typical flow for Pact testing using HTTP interactions, written in the XUnit framework.

A more comprehensive example which uses both HTTP and message interactions, provider states, matchers and more can be found in the samples/OrdersApi folder.

Writing a Consumer test

Pact is a consumer-driven contract testing tool, which is a fancy way of saying that the API Consumer writes a test to set out its assumptions and needs of its API Provider(s). By unit testing our API client with Pact, it will produce a contract that we can share to our Provider to confirm these assumptions and prevent breaking changes.

In this example, we are going to be testing our User API client, responsible for communicating with the UserAPI over HTTP. It currently has a single method GetUser(id) that will return a User.

Pact tests have a few key properties. We'll demonstrate a common example using the 3A Arrange/Act/Assert pattern.

public class SomethingApiConsumerTests
{
    private readonly IPactBuilderV4 pactBuilder;

    public SomethingApiConsumerTests()
    {
        // Use default pact directory ..\..\pacts and default log
        // directory ..\..\logs
        var pact = Pact.V4("Something API Consumer", "Something API", new PactConfig());

        // or specify custom log and pact directories
        pact = Pact.V4("Something API Consumer", "Something API", new PactConfig
        {
            PactDir = $"{Directory.GetParent(Directory.GetCurrentDirectory()).Parent.Parent.Parent.FullName}{Path.DirectorySeparatorChar}pacts"
        });

        // Initialize Rust backend
        this.pactBuilder = pact.WithHttpInteractions();
    }

    [Fact]
    public async Task GetSomething_WhenTheTesterSomethingExists_ReturnsTheSomething()
    {
        // Arrange
        this.pactBuilder
            .UponReceiving("A GET request to retrieve the something")
                .Given("There is a something with id 'tester'")
                .WithRequest(HttpMethod.Get, "/somethings/tester")
                .WithHeader("Accept", "application/json")
            .WillRespond()
                .WithStatus(HttpStatusCode.OK)
                .WithHeader("Content-Type", "application/json; charset=utf-8")
                .WithJsonBody(new
                {
                    id = "tester",
                    firstName = "Totally",
                    lastName = "Awesome"
                });

        await this.pactBuilder.VerifyAsync(async ctx =>
        {
            // Act
            var client = new SomethingApiClient(ctx.MockServerUri);
            var something = await client.GetSomething("tester");

            // Assert
            Assert.Equal("tester", something.Id);
        });
    }
}

----------

Verifying a Provider

A provider test takes one or more pact files (contracts) as input, and Pact verifies that your provider adheres to the contract. In the simplest case, you can verify a provider as per below. In SomethingApiFixture, the provider is started. In SomethingApiTests, the fixture is verified against the pact files.

public class SomethingApiFixture : IDisposable
{
    private readonly IHost server;
    public Uri ServerUri { get; }

    public SomethingApiFixture()
    {
        ServerUri = new Uri("http://localhost:9223");
        server = Host.CreateDefaultBuilder()
                     .ConfigureWebHostDefaults(webBuilder =>
                     {
                         webBuilder.UseUrls(ServerUri.ToString());
                         webBuilder.UseStartup<TestStartup>();
                     })
                     .Build();
        server.Start();
    }

    public void Dispose()
    {
        server.Dispose();
    }
}

public class SomethingApiTests : IClassFixture<SomethingApiFixture>
{
    private readonly SomethingApiFixture fixture;
    private readonly ITestOutputHelper output;

    public SomethingApiTests(SomethingApiFixture fixture, ITestOutputHelper output)
    {
        this.fixture = fixture;
        this.output = output;
    }

    [Fact]
    public void EnsureSomethingApiHonoursPactWithConsumer()
    {
        // Arrange
        var config = new PactVerifierConfig
        {
            Outputters = new List<IOutput>
            {
                // NOTE: PactNet defaults to a ConsoleOutput, however
                // xUnit 2 does not capture the console output, so this
                // sample creates a custom xUnit outputter. You will
                // have to do the same in xUnit projects.
                new XUnitOutput(output),
            },
        };

        string pactPath = Path.Combine("..",
                                       "..",
                                       "..",
                                       "..",
                                       "pacts",
                                       "Something API Consumer-Something API.json");

        // Act / Assert
        using var pactVerifier = new PactVerifier("Something API", config);

        pactVerifier
            .WithHttpEndpoint(fixture.ServerUri)
            .WithFileSource(new FileInfo(pactPath))
            .WithProviderStateUrl(new Uri(fixture.ServerUri, "/provider-states"))
            .Verify();
    }
}

IMPORTANT: You can't use the Microsoft.AspNetCore.Mvc.Testing library to host your API for provider tests. If your tests are using TestServer or WebApplicationFactory then these are running the API with a special in-memory test server instead of running on a real TCP socket. This means the Rust internals can't call the API and therefore all of your provider tests will fail. You must host the API on a proper TCP socket, e.g. by using the Host method shown in the sample above, so that they can be called from non-.Net code.

----------

Messaging Pacts

For writing messaging pacts instead of requests/response pacts, see the messaging pacts guide.

----------

Compatibility

Operating System

Due to using a shared native library instead of C# for the main Pact logic only certain OSs are supported:

OS Arch Support
Windows x86 ❌ No
Windows x64 ✔️ Yes
Linux (libc) ARM64 ✔️ Yes
Linux (libc) x64 ✔️ Yes
Linux (libc) x86 ❌ No
Linux (musl) Any No
OSX x64 ✔️ Yes
OSX ARM64 (M1/M2) ✔️ Yes

Pact Specification

Version Status Spec Compatibility Install
5.x Stable 2, 3, 4 See installation
4.x Deprecated 2, 3 See installation
3.x Deprecated 2

Roadmap

The roadmap for Pact and Pact Net is outlined on our main website.

Contributing

See CONTRIBUTING.