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.
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 , 2006Why 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
- Define the Consumer Test: Blazor checks if it can parse the specific response it expects.
- Generate the Pact File: A JSON file describing the request/response pairs.
- 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.
[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.