Backend Intermediate 10 min

Consumer-Driven Contract Testing with Pact

Decoupling frontend and backend tests by defining API contracts. How we verify Blazor/FastEndpoint compatibility in CI/CD without integration environments.

By Victor Robin Updated:

When I first started building our Blazor frontend against the FastEndpoints API, deployments were a recurring source of anxiety. More than once, a seemingly innocent backend change — renaming a JSON property or changing a date format — broke the frontend in production. Traditional integration tests caught some issues, but they required spinning up the entire infrastructure stack and were painfully slow. Discovering consumer-driven contract testing with Pact was a turning point that gave me confidence to ship independently without fear of breaking the contract between services.

Introduction

In a distributed system, integration testing is painful. You have to spin up the Database, the API, the Identity Provider, and the event bus just to check if the Frontend can successfully call GET /documents.

Consumer-Driven Contract Testing (CDCT) solves this by flipping the model. The Consumer (Blazor App) defines what it needs, and the Provider (FastEndpoints API) verifies it can fulfill that need.

[Consumer-Driven Contracts: A Service Evolution Pattern] — Ian Robinson , 2006

Why Pact Matters:

  • Faster Feedback: No need to deploy to test.
  • Decoupling: Teams can move independently as long as the contract holds.
  • Documentation: The contract (Pact file) serves as live API documentation.

What We’ll Build

  1. Define the Consumer Test: Blazor checks if it can parse the specific response it expects.
  2. Generate the Pact File: A JSON file describing the request/response pairs.
  3. Verify the Provider: The API runs a test suite that replays these requests against itself.

Architecture Overview

sequenceDiagram
    participant Blazor as Blazor (Consumer)
    participant Mock as Pact Mock Server
    participant PactFile as Pact JSON
    participant API as API (Provider)

    Note over Blazor, Mock: 1. Consumer Test
    Blazor->>Mock: Request /documents/123
    Mock-->>Blazor: Returns { "id": "123", "title": "Test" }
    Mock->>PactFile: Generates Contract

    Note over PactFile, API: 2. Provider Verification
    PactFile->>API: Replays Request /documents/123
    API-->>PactFile: Real Response
    PactFile->>PactFile: Matches?
[Pact Documentation: How Pact Works] — Pact Foundation , 2024

Section 1: The Consumer Test (Blazor)

We use PactNet. In our Blazor unit test project, we set up a mock server.

[PactNet: .NET Implementation of Pact] — Pact Foundation , 2024
[Fact]
public async Task GetDocument_ReturnsCorrectData()
{
    // Arrange
    var pact = Pact.V3("MyApp.Web", "MyApp.Api", Config);

    await pact.UponReceiving("A valid request for a document")
        .Given("Document 123 exists")
        .WithRequest(HttpMethod.Get, "/api/documents/123")
        .WillRespond()
        .WithStatus(HttpStatusCode.OK)
        .WithJsonBody(new { id = "123", title = Match.Type("Contract.pdf") });

    // Act
    await pact.VerifyAsync(async ctx =>
    {
        var client = new DocumentClient(ctx.MockServerUri);
        var result = await client.GetDocumentAsync("123");

        Assert.Equal("123", result.Id);
    });
}

This acts as a unit test for our HTTP Client code, but it also outputs a pact.json file.

Section 2: The Provider Verification (API)

On the backend side, we don’t write new tests. We simply tell Pact to verify the generated file against our running API (or an in-memory test server).

public class ApiContractTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public void EnsureApiHonorsPact()
    {
        var config = new PactVerifierConfig();

        IPactVerifier verifier = new PactVerifier(config);

        verifier
            .ServiceProvider("MyApp.Api", _factory.Server.BaseAddress)
            .HonoursPactWith("MyApp.Web")
            .PactUri("path/to/pact.json") // Or from Pact Broker
            .Verify();
    }
}
[Contract Testing vs Integration Testing] — PactFlow , 2023 [Pact Broker: Contract Management] — Pact Foundation , 2024

Conclusion

With Pact, we catch breaking changes at build time. If the backend developer renames title to documentTitle, the Provider Verification step fails immediately, well before deployment.

Adopting contract testing fundamentally changed how our team collaborates. Before Pact, frontend and backend developers had to synchronize deployments and cross their fingers that nothing would break at the integration boundary. Now, each team ships independently with a confidence that comes from knowing the contract is verified on every build. The initial investment in setting up PactNet and the Pact Broker was modest compared to the hours of debugging and emergency hotfixes it has prevented. If you work in a system with multiple services communicating over HTTP, contract testing should be a non-negotiable part of your testing strategy.

Next Steps

  • After verification, run Microservices Stress Testing to ensure performance under load.
  • Learn how we optimize the deployment in Optimizing Kubernetes Images.
  • Extend contract testing to cover asynchronous messaging contracts (NATS events) using Pact’s message interaction support.
  • Set up “can-i-deploy” gates in your CI/CD pipeline to prevent deploying incompatible service versions.

Further Reading

[Pact Documentation: Getting Started] — Pact Foundation , 2024 [Contract Testing in Microservices by Sam Newman] — Sam Newman , 2024 [PactFlow: Enterprise Contract Testing Platform] — Pactflow , 2024 [Testing Microservices with Consumer-Driven Contracts (ThoughtWorks)] — Thoughtworks , 2024