Integration · Microservices

Contract Testing

Contract testing (PACT) verifies that two services agree on the shape and meaning of the data they exchange, before they ever talk to each other in integration tests or production. Catch breaking changes early, in isolation, and in parallel.

Senior Test Lead

What it is

Contract testing is a pattern for verifying that two independent services (a consumer and a provider) can communicate correctly, without needing to deploy them together or mock every variant of their interaction. Instead, the consumer defines what it needs from the provider (a “contract”), and the provider verifies it can satisfy that contract — all in parallel, in CI, long before either service touches production.

The core insight: integration tests are expensive and slow because you have to run both services together. Contract tests split that job. Consumers test in isolation (mocking the provider), providers test in isolation (verifying real code against consumer expectations), and both can run in parallel in CI. You only test the real integration once, briefly, in staging.

Why not just mock? Mocks let the consumer and provider diverge silently. A contract makes the agreement explicit and testable: if the provider changes and breaks the contract, the test fails immediately, before the code is merged.

Why it matters

In a monolith, you have one integration test suite that catches breaking changes. In microservices, you have many services, each with its own deployment schedule. Without contract tests, the only way to catch a breaking change is to deploy both services and run end-to-end tests — slow, expensive, and you don’t know which service is at fault.

Contract tests shift that burden left: the provider knows immediately (in their own CI) whether they have broken any consumer, because each consumer has published its contract. The consumer knows immediately whether the provider has changed in a way that breaks them. Both happen in CI, in parallel, without waiting for integration environments.

Consumer-driven contracts

A contract is consumer-driven: the consumer defines what it needs from the provider, and the provider verifies it can deliver.

  • Consumer writes a test that describes what it expects from the provider: “When I POST /orders with {customerId: 123, total: 49.99}, I expect a 201 response with {orderId: ..., status: 'pending'}.”
  • The test runs with a mock provider (using PACT or similar), verifies the consumer code works, and generates a contract file (JSON).
  • The contract is published to a contract broker (a shared repository).
  • The provider pulls the contract from the broker and tests their real code against it: “Does my /orders endpoint actually return what this consumer expects?”
  • If the provider breaks the contract, their CI fails immediately. They either fix the code or negotiate a new contract with the consumer.

PACT framework overview

PACT is the most widely used contract testing framework. It works in four steps:

PACT contract testing flow
StepWhoWhat happensOutput
1. Consumer test Consumer team Consumer code tests against a PACT mock provider. The test exercises the consumer code: “Call the payment service, expect a 200 and {transactionId: ...}” Contract JSON file (pact)
2. Publish Consumer CI The pact JSON is published to a contract broker (e.g. PactFlow, Pact Broker) Contract stored in broker
3. Provider verify Provider team Provider CI pulls all contracts from the broker and tests the real provider code against each one: “Does my payment service return what all consumers expect?” Pass/fail per contract
4. Deploy with confidence Both teams If all contracts pass, both services can deploy independently. No surprises in production. Safer deployment

When to use it

  • Microservices with multiple consumers — one provider, many consumers. Each consumer defines its own contract; the provider verifies all of them.
  • Third-party API integration — you cannot deploy the third-party service, but you can write consumer contracts against their published API and catch breaking changes in CI.
  • Parallel deployment — teams deploy independently. Contract tests ensure you don’t break downstream services without knowing.
  • Not suitable for monoliths — in a monolith, you have one integration test suite; contract tests don’t add value. Use contract tests when services are independently deployable.

Contract testing process

Consumer side

The consumer test is a unit test that mocks the provider and verifies the consumer code works correctly:

  • Set up a PACT mock provider on a local port.
  • Write a test: “When I call the payment service with {amount: 50, currency: 'NZD'}, I expect a 200 with {transactionId: ..., status: 'success'}.”
  • The mock provider records what the consumer expects.
  • Run the consumer code and verify it works.
  • The test framework writes a pact file (JSON) describing the contract.
  • Publish the pact file to the contract broker.

Provider side

The provider verification test pulls contracts from the broker and tests the real code:

  • Pull all pact files from the contract broker.
  • For each pact, extract the requests the consumer expects.
  • Send those requests to the real provider code (running locally).
  • Verify the responses match the pact exactly (status code, headers, body schema, data types).
  • If the real provider breaks the contract, the test fails. The provider team must either fix the code or negotiate with the consumer.

Worked example: Order and Payment services

Imagine an e-commerce system with two services:

  • Order Service — creates orders, calls Payment Service to charge the customer.
  • Payment Service — processes payments, returns transaction IDs.
Consumer contract: Order Service expects from Payment Service
{
  "consumer": {
    "name": "OrderService"
  },
  "provider": {
    "name": "PaymentService"
  },
  "interactions": [
    {
      "description": "a valid payment request",
      "request": {
        "method": "POST",
        "path": "/payments",
        "body": {
          "orderId": "ORD-123",
          "amount": 49.99,
          "currency": "NZD"
        },
        "headers": {
          "Content-Type": "application/json"
        }
      },
      "response": {
        "status": 201,
        "body": {
          "transactionId": "TXN-456",
          "orderId": "ORD-123",
          "status": "success",
          "amount": 49.99
        },
        "headers": {
          "Content-Type": "application/json"
        }
      }
    }
  ]
}

The Order Service CI publishes this pact to the contract broker. Then, the Payment Service CI pulls it and tests:

Provider verification: Payment Service verifies the contract
// Payment Service verifies the contract
@Test
public void testOrderServiceContract() {
  // PACT framework pulls the contract from broker
  // Sends the request: POST /payments with {orderId, amount, currency}
  // Gets the real response from Payment Service code
  // Compares: response status 201? body has transactionId? amount matches?

  // If Payment Service code changed the response to:
  // status: 200 (should be 201)
  // body: {txId: ...} (consumer expects "transactionId", not "txId")
  // Then: TEST FAILS. Provider team must fix.
}

If the Payment Service test passes, the provider team knows they have not broken any consumer. If it fails, they know exactly which consumer expects what, and they must fix it before deploying.

Tools

  • PACT — the standard, open-source contract testing framework. Available for Java, JavaScript, Go, Python, .NET, and more.
  • PactFlow — managed SaaS contract broker; hosts pact files, integrates with CI/CD, provides visibility.
  • Pact Broker — open-source contract broker you can self-host.
  • Spring Cloud Contract — Java-specific contract testing, similar to PACT but closer to Spring ecosystem.
  • Swagger / OpenAPI — not contract testing per se, but API specs can be verified against contracts.

Common pitfalls and best practices

  • Over-mocking the provider — the mock provider should return exactly what the real provider will return. If your mock is too permissive, the contract will pass but the real provider will fail.
  • Testing too much behavior in contracts — contracts should specify the API boundary (request/response shape), not internal business logic. Leave complex behaviour to unit tests.
  • Forgetting to verify contracts in CI — if the provider doesn’t actually verify contracts in CI, breaking changes will slip through. Make it mandatory.
  • Not publishing contracts often enough — publish whenever the consumer changes its expectations, not just on release. This gives the provider feedback in real time.
  • Ignoring contract failure — if a provider test fails because they broke a contract, they must fix it. Don’t ignore the failure or bypass the test.
  • Treating contracts as the only integration test — contracts verify that the API boundary is correct, but they don’t test end-to-end scenarios. Still run integration tests, but less frequently (after contract tests pass).

Key principle: Contract tests are not a replacement for integration or end-to-end tests. They are a fast, parallel way to catch breaking changes before integration tests run. Think of them as unit tests for the API boundary.