Skip to content
Back to Quality Assurance

Contract Testing

Verify that a consumer's expectations and a provider's responses agree, without running the full integration.

Why

In a microservices or third-party-API world, full integration tests for every consumer-provider pair are prohibitively expensive — you would need every service running together. Contract testing replaces that with a lightweight exchange: the consumer records the shape of response it expects (the contract), and the provider verifies it always produces that shape. This catches breaking changes at the provider before they reach any consumer, and it does so in milliseconds per contract rather than minutes per full integration. Pact and Spring Cloud Contract are the established tools; the pattern is provider-driven (OpenAPI) or consumer-driven (Pact).

When

When you have more than one service or a third-party API dependency. Set up consumer-driven contracts before the first breaking change bites you — retrofitting contracts onto an existing integration is painful because every endpoint has already drifted.

How

1. Choose the direction: consumer-driven (Pact) or provider-driven (OpenAPI)
   - Consumer-driven: consumer writes expectations, provider verifies against them
   - Provider-driven: provider publishes an OpenAPI spec, consumers generate stubs

2. (Consumer-driven) Write the consumer pact test
   - In the consumer's test suite, define the request and expected response
   - The test runs against a mock provider and generates a pact JSON file
   - This file IS the contract — it is what the provider will verify

3. Publish the pact to a broker (Pact Broker or Pactflow)
   - The broker stores contracts tagged with consumer version and environment
   - It tracks which contracts have been verified by which provider versions

4. (Provider) Verify the pact in the provider's CI
   - Replay each pact request against the real provider endpoint
   - If the response shape does not match the pact, the build fails
   - Publish verification results back to the broker

5. Gate deploys on "can-i-deploy" checks
   - Before deploying a consumer, ask the broker: "does the provider version
     in production already verify my contract?" If no, block the deploy

Example

// consumer: tests/orders-api.pact.test.ts (Pact JS)
import { Pact } from "@pact-foundation/pact";
import path from "path";

const pact = new Pact({
  consumer: "web-frontend",
  provider: "orders-api",
  dir: path.resolve(__dirname, "pacts"),
});

describe("Orders API contract", () => {
  beforeAll(() => pact.setup());
  afterAll(() => pact.finalize());

  it("returns an order by id", async () => {
    await pact.addInteraction({
      uponReceiving: "a request for an order",
      withRequest: { method: "GET", path: "/orders/123" },
      willRespondWith: {
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: { id: 123, status: "shipped", total: 49.99 },
      },
    });
    const order = await fetchOrder(123); // consumer's real client call
    expect(order).toEqual({ id: 123, status: "shipped", total: 49.99 });
  });
});

// provider: verify the pact in CI
// npx pact-verifier --pact-url=./pacts/web-frontend-orders-api.json
//   --provider-base-url=http://localhost:8080 --publish-verification-results

Common Problems

  • ⚠Contracts that only verify the happy path (200 response) miss every error-shape change — the provider starts returning a 422 with a different error body and the consumer's error handling silently breaks.
  • ⚠Publishing pacts but never running verification on the provider side gives false confidence — the contract exists but nobody checks that the provider actually honors it.
  • ⚠Not tagging pacts with the consumer's deployed environment means the can-i-deploy check cannot distinguish "verified against staging" from "verified against production," and breaking changes reach prod.
  • ⚠Over-specifying the contract (every field, every nested object, exact string values) makes it so brittle that any harmless addition of a new optional field breaks the verification.

Best Practices

  • ✓Use matchers (like, eachLike, term) instead of exact values in pact bodies — assert the shape and type, not the specific data, so the contract survives legitimate data changes.
  • ✓Include at least one error-path contract per endpoint (404 for missing, 422 for invalid input) so the consumer's error handling is also covered by the agreement.
  • ✓Wire can-i-deploy checks into the deploy pipeline as a hard gate — a consumer should never deploy if its contract is not verified by the provider version already in the target environment.
  • ✓Publish verification results to the broker on every provider CI run so the dashboard always shows the current compatibility state across all consumer-provider pairs.