Course contentsShow
← C#
Lesson 1662 of 1,90815. Software Quality and TestingFree lesson

Client-Server Contract Testing

Use contract testing to ensure breaking changes are caught between microservices.

In this lesson, you will learn how to catch breaking API changes before they reach production by implementing consumer-driven contract testing.

The Problem: Integration Fragility

When you rely on WebApplicationFactory for integration tests, you are testing the actual code path of your server. However, if your server team changes a JSON field name or removes an endpoint, your client (another microservice) might not realize the contract is broken until it crashes in production. Relying solely on E2E tests is slow and brittle; contract testing solves this by defining a shared "contract" that both parties must satisfy.

Defining the Contract

Instead of mocking the HttpClient with complex HttpMessageHandler setups, we use a contract file (often a JSON document) that defines the expected request/response format.

A popular approach in the C# ecosystem is using tools like Pact. The consumer defines the expected interaction:

// Define the interaction in the Consumer test
pact.UponReceiving("A request for a user")
    .WithRequest(HttpMethod.Get, "/users/1")
    .WillRespondWith(HttpStatusCode.OK, new {
        id = 1,
        username = "jdoe" // If the server changes this to "name", the test fails!
    });

Validating the Provider

Once the consumer defines this contract, the Pact file is shared with the Provider (the API service). The Provider then runs a test suite that replays this request against its own controllers. If the Provider's current implementation doesn't return the exact schema defined in the contract, the test fails immediately during the CI/CD pipeline.

By decoupling the tests from the actual network call and focusing on the schema agreement, you gain two major benefits:

  1. Speed: You don't need to spin up the entire database or complex dependencies.
  2. Safety: You receive an immediate "build failed" notice if a developer changes a model that a downstream service depends on, preventing cross-service breaking changes.

Contract testing acts as a formal communication layer between teams, ensuring that your HttpClient requests and your Controller responses remain in perfect sync without the overhead of full integration suites.

Key Takeaway: Contract testing shifts the focus from testing the implementation to verifying the shared schema, ensuring that breaking changes are caught at the boundary of your microservices before deployment.