Pact Net

repository·master·Indexed 21 days ago

https://github.com/pact-foundation/pact-net

A consumer-driven contract testing tool for .NET that replaces brittle end-to-end integration tests with fast, reliable contract tests for HTTP/REST and event-driven systems. It supports Pact Specification v3 via a Rust core library and provides tools for both consumer-side interaction definition and provider-side verification. The library is available via the PactNet NuGet package and supports Windows (x64), Linux (libc x64/ARM64), and OSX (x64/ARM64).

Tokens
7.2K
Snippets
9
Records
15
Agent score
76%

What's inside Pact Net

  1. How Messaging Pacts work

    master

    Messaging pacts allow a consumer and a producer to agree on the structure of messages (including metadata and content) without requiring a specific transport protocol like HTTP.

    Key Concepts:

    • Consumer: Defines the messages it expects to receive.
    • Producer: Is verified to ensure the messages it produces match the consumer's expectations.
    • Transport Agnostic: PactNet does not implement specific transports (like Kafka, RabbitMQ, or ZeroMQ). Instead, messages are simulated internally during the provider verification stage.

    Important Constraint: To avoid conflicts, ensure that a consumer or provider name is not used for both request/response (HTTP) pacts and messaging pacts. If a service uses both, use distinct names, for example:

    • Stock Broker API (for HTTP)
    • Stock Broker Messaging (for messages)
  2. Verify combined HTTP and Messaging pacts in PactNet 5.x

    master

    Pact Specification v4 allows a single Pact file to contain both HTTP and messaging interactions. In PactNet 5.x, you no longer need separate verifiers for HTTP and Messaging. You can now use a single PactVerifier instance to configure both an HTTP endpoint and message scenarios.

    Instead of using ServiceProvider(...).WithFileSource(...) for HTTP and MessagingProvider(...).WithProviderMessages(...) for messaging separately, use the unified PactVerifier API.

    var verifier = new PactVerifier("My API");
    
    verifier
        .WithHttpEndpoint(new Uri("http://localhost:5000"))
        .WithMessages(scenarios =>
        {
            scenarios.Add<MyEvent>("an event happens")
        })
        .WithFileSource(new FileInfo(@"..."))
        .Verify();
  3. Upgrade to PactNet 4.x

    master

    PactNet 4.0.0 introduced a major rewrite based on a Rust core library. This version supports Pact Specification v3, offers improved performance, and uses an in-process mock server.

    To upgrade from v3.x to v4.x or later:

    1. Uninstall OS-specific NuGets: Remove any packages like PactNet.Windows.
    2. Install the unified NuGet: Install only the PactNet NuGet package at version 4.0.0 or greater.
    3. Migrate Tests: Update your consumer and provider tests to the new fluent API and lifecycle models described below.
  4. Write Consumer Tests in PactNet 4.x

    master

    Consumer tests in v4.x use a new fluent API and an in-process mock server. Key changes from v3.x include:

    • Independent Test Execution: Each test runs its own mock server instance. You no longer need IClassFixture or ClearInteractions() between tests.
    • Automatic Port Assignment: A free port is automatically assigned to the mock server; you do not need to manually manage ports.
    • VerifyAsync Lifecycle: All API calls must occur inside the lambda passed to VerifyAsync. The mock server starts when VerifyAsync is called and shuts down when it returns. The ctx (context) argument provides the MockServerUri.
    • Fluent API: Expectations are defined by chaining .UponReceiving(...).WithRequest(...).WillRespond().WithStatus(...).
    • Merge Mode: Pact files are written in merge mode. To avoid stale interactions in CI, ensure your CI environment deletes existing pact files before running tests.

    Example setup using IPactV4 and PactConfig:

    public class ConsumerTests
    {
        private readonly IPactBuilderV4 pact;
    
        public ConsumerTests(ITestOutputHelper output)
        {
            var config = new PactConfig
            {
                PactDir = "../../../pacts/",
                Outputters = new[] { new XUnitOutput(output) },
                DefaultJsonSettings = new JsonSerializerOptions
                {
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                }
            };
    
            // Select specification version (V2, V3, or V4)
            IPactV4 pact = Pact.V4("My Consumer", "My Provider", config);
    
            // Create the builder using the native backend
            this.pact = pact.UsingNativeBackend();
        }
    
        [Fact]
        public async Task GetAllEvents_WhenCalled_ReturnsAllEvents()
        {
            // 1. Define expectations using the fluent API
            this.pact
                .UponReceiving("a request to retrieve all events")
                    .WithRequest(HttpMethod.Get, "/events")
                    .WithHeader("Accept", "application/json")
                .WillRespond()
                    .WithStatus(HttpStatusCode.OK)
                    .WithHeader("Content-Type", "application/json; charset=utf-8")
                    .WithJsonBody(Match.MinType(new
                    {
                        eventId = Match.Type("3E83A96B-2A0C-49B1-9959-26DF23F83AEB"),
                        timestamp = Match.Type("2014-06-30T01:38:00.8518952"),
                        eventType = Match.Regex("SearchView", "SearchView|DetailsView")
                    }, 1));
    
            // 2. Execute the test inside VerifyAsync
            await this.pact.VerifyAsync(async ctx =>
            {
                // Use ctx.MockServerUri to point your client to the mock server
                var client = new EventsApiClient(ctx.MockServerUri);
                IEnumerable<Event> events = await client.GetAllEvents();
                events.Should().BeEquivalentTo(new[] { example });
            });
        }
    }
  5. Write a Consumer test with Pact Net

    master

    In consumer-driven contract testing, the Consumer defines its expectations of the Provider. You use Pact.V4 to initialize a pact and WithHttpInteractions() to start building HTTP-based contracts.

    Tests typically follow the Arrange/Act/Assert pattern:

    1. Arrange: Use .UponReceiving(...).Given(...).WithRequest(...).WillRespond()... to define the interaction.
    2. Act: Call pactBuilder.VerifyAsync(...). Inside the callback, execute the actual code that calls the API using the provided ctx.MockServerUri.
    3. Assert: Use standard assertion libraries (like xUnit) to verify the results returned by your client.
    public class SomethingApiConsumerTests
    {
        private readonly IPactBuilderV4 pactBuilder;
    
        public SomethingApiConsumerTests()
        {
            // Initialize Pact V4 with default or custom config
            var pact = Pact.V4("Something API Consumer", "Something API", new PactConfig());
    
            // Initialize Rust backend for HTTP interactions
            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);
            });
        }
    }
  6. Verify Providers with PactNet 4.x

    master

    Provider verification in v4.x uses the PactVerifier class. You can verify against a single file, a directory, or a Pact Broker. When using a Pact Broker, you can use WithPactBrokerSource to configure consumer version selectors and automatically publish verification results back to the broker.

    Example of verifying against a Pact Broker and publishing results:

    [Fact]
    public void VerifyLatestPacts()
    {
        var verifier = new PactVerifier("My Provider", new PactVerifierConfig
        {
            LogLevel = PactLogLevel.Information,
            Outputters = new List<IOutput> { new XUnitOutput(this.output) }
        });
    
        verifier.WithHttpEndpoint(this.fixture.ServerUri)
                .WithPactBrokerSource(new Uri("https://broker.example.org"), options =>
                {
                    options.ConsumerVersionSelectors(new ConsumerVersionSelector { MainBranch = true, Latest = true })
                           .PublishResults(version, results =>
                           {
                               results.ProviderBranch(branch)
                                      .BuildUri(new Uri(buildUri));
                           });
                })
                .WithProviderStateUrl(new Uri(this.fixture.ServerUri, "/provider-states"))
                .Verify();
    }
  7. Migrate from Newtonsoft.Json to System.Text.Json in PactNet 5.x

    master

    PactNet 5.x has replaced Newtonsoft.Json with System.Text.Json for all serialization and deserialization tasks. This affects how message interactions are defined and verified.

    • Configuration: Replace JsonSerializerSettings with JsonSerializerOptions where applicable.
    • Type Annotations: If you were using both Newtonsoft.Json and System.Text.Json attributes on your models to support both ASP.NET Core and PactNet, you can now likely remove the Newtonsoft.Json attributes.
    • Message Content: When using .WithJsonContent(...) in consumer tests or scenarios.Add<T>(...) in provider verifications, the content is now processed via System.Text.Json.

    Note: Ensure your models are compatible with System.Text.Json serialization rules to avoid mismatches during verification.

    // Example of a messaging interaction using System.Text.Json
    [Fact]
    public async Task OnMessageAsync_OrderCreated_HandlesMessage()
    {
        await this.pact
                  .ExpectsToReceive("an event indicating that an order has been created")
                  // This content is now serialized using System.Text.Json
                  .WithJsonContent(new
                  {
                      Id = Match.Integer(1)
                  })
                  // This type will now be deserialized using System.Text.Json
                  .VerifyAsync<OrderCreatedEvent>(async message =>
                  {
                      await this.consumer.OnMessageAsync(message);
                      this.mockService.Verify(s => s.FulfilOrderAsync(message.Id));
                  });
    }
  8. Replace obsolete Pact interfaces and methods in PactNet 5.x

    master

    In PactNet 5.0, several interfaces and extension methods used for messaging and native backends have been removed.

    • Remove IMessagePact and MessagePact: These are no longer supported. Use IPact and the Pact implementation instead.
    • Replace UsingNativeBackend: The extension methods UsingNativeBackend for both IPact and IMessagePact have been removed.
      • Use IPact.WithHttpInteractions for HTTP-based interactions.
      • Use IPact.WithMessageInteractions for messaging-based interactions.
  9. Verify a Provider with Pact Net

    master

    Provider verification ensures that the Provider adheres to the contracts (pact files) generated by consumers.

    Key Requirements:

    • TCP Socket Hosting: You cannot use Microsoft.AspNetCore.Mvc.Testing (e.g., WebApplicationFactory or TestServer) because they use an in-memory server. Pact's native internals require the API to be hosted on a real TCP socket to communicate with it.
    • Provider States: You must provide a WithProviderStateUrl so the verifier can tell the provider how to set up the necessary data context before running interactions.

    Workflow:

    1. Start your provider service on a specific port (e.g., using IHost).
    2. Use PactVerifier to point to the provider's name, the HTTP endpoint, the pact file source, and the provider states URL.
    3. Call .Verify() to execute the verification.
    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>
                {
                    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();
        }
    }
  10. Write Consumer tests for Messaging Pacts

    master

    To write consumer tests for messages, use IPactV4.WithMessageInteractions(). This allows you to specify the expected message description, provider states (Given), metadata, and JSON content structure using matchers. Once tests pass, PactNet generates a pact file containing these interactions for the provider to verify.

    Use WithMetadata for headers/metadata and WithJsonContent with Match utilities to define the expected shape of the message payload.

    public class StockEventProcessorTests
    {
        private readonly IMessagePactBuilderV4 messagePact;
    
        public StockEventProcessorTests(ITestOutputHelper output)
        {
            IPactV4 v4 = Pact.V4("Stock Event Consumer", "Stock Event Producer", new PactConfig
            {
                PactDir = "../../../pacts/",
                DefaultJsonSettings = new JsonSerializerOptions
                {
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
                },
                Outputters = new[]
                {
                    new XUnitOutput(output)
                }
            });
    
            this.messagePact = v4.WithMessageInteractions();
        }
    
        [Fact]
        public void ReceiveSomeStockEvents()
        {
            this.messagePact
                .ExpectsToReceive("some stock ticker events")
                .Given("A list of events is pushed to the queue")
                .WithMetadata("key", "valueKey")
                .WithJsonContent(Match.MinType(new
                {
                    Name = Match.Type("AAPL"),
                    Price = Match.Decimal(1.23m),
                    Timestamp = Match.Type(14.February(2022).At(13, 14, 15, 678))
                }, 1))
                .Verify<ICollection<StockEvent>>(events =>
                {
                    events.Should().BeEquivalentTo(new[]
                    {
                        new StockEvent
                        {
                            Name = "AAPL",
                            Price = 1.23m,
                            Timestamp = 14.February(2022).At(13, 14, 15, 678)
                        }
                    });
                });
        }
    }
  11. Verify Messaging Pacts on the Provider side

    master

    Provider verification for messaging pacts requires registering a handler for each interaction defined in the pact file. These handlers must generate a sample message that matches the expected interaction description.

    Steps:

    1. Initialize PactVerifier with the provider name.
    2. Use .WithMessages() to register scenarios. The description provided in .Add("description", ...) must exactly match the description used in the consumer's pact file.
    3. Use .WithFileSource() to point to the generated pact JSON file.
    4. Call .Verify() to run the verification.

    Note: Always call Dispose() on the PactVerifier to stop the internal messaging server used during simulation.

    public class StockEventGeneratorTests : IDisposable
    {
        private readonly PactVerifier verifier;
    
        public StockEventGeneratorTests()
        {
            this.verifier = new PactVerifier("Stock Event Producer");
        }
    
        public void Dispose()
        {
            // make sure you dispose the verifier to stop the internal messaging server
            GC.SuppressFinalize(this);
            this.verifier.Dispose();
        }
    
        [Fact]
        public void EnsureEventApiHonoursPactWithConsumer()
        {
            string pactPath = Path.Combine("..",
                                           "..",
                                           "..",
                                           "..",
                                           "Consumer.Tests",
                                           "pacts",
                                           "Stock Event Consumer-Stock Event Producer.json");
    
            var defaultSettings = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
    
            this.verifier
                .WithMessages(scenarios =>
                {
                    // register the responses to each interaction
                    // the descriptions must match those in the pact file(s)
                    scenarios.Add("a single event", () => new StockEvent
                             {
                                 Name = "AAPL",
                                 Price = 1.23m
                             })
                             .Add("some stock ticker events", builder =>
                             {
                                 builder.WithMetadata(new
                                         {
                                             ContentType = "application/json",
                                             Key = "value"
                                         })
                                         .WithContent(()=>new[]
                                         {
                                             new StockEvent { Name = "AAPL", Price = 1.23m },
                                             new StockEvent { Name = "TSLA", Price = 4.56m }
                                         });
                             });
                }, defaultSettings)
                .WithFileSource(new FileInfo(pactPath))
                .Verify();
        }
    }