top of page

Contract Testing for Microservices

  • Contributor
  • Apr 22
  • 5 min read

In a microservices architecture, services depend on each other. The naive way to verify integration is to run them all together end-to-end. That works for two services. For twenty, it's a nightmare. Contract testing offers an alternative: verify each service against the agreed-upon contract, independently.

The Problem

Service A calls Service B. They agree on a contract — the API shape, status codes, response format. Over time, B's team changes B's API. They run their tests; everything passes. They deploy. A breaks.

The problem: B's tests verify B against B's own assumptions. They don't verify B against A's expectations.

End-to-end tests catch this but are slow and brittle. Contract tests catch it without the cost.

The Idea

Each consumer (A) declares the contract it expects from the provider (B). The provider verifies, in its own test suite, that it satisfies those contracts.

Two test runs:

  • Consumer test: A's tests run against a mock of B that returns what the contract says. Verifies A handles the contract correctly.

  • Provider test: B's tests run the contract against the real B. Verifies B produces what the contract promises.

If both pass, A and B can talk to each other without ever running together.

How It Works (with Pact, as Example)

A typical flow:

  1. Consumer side (A): A's tests describe interactions with B

  2. Pact generated: when A's tests run, they produce a contract file (a "pact")

  3. Pact published: the file is uploaded to a Pact Broker

  4. Provider side (B): B's tests pull the pact and verify B satisfies it

  5. Both verified: if both pass, the integration works

pact.given('a user exists')
  .uponReceiving('a request for the user')
  .withRequest({ method: 'GET', path: '/users/123' })
  .willRespondWith({ 
    status: 200, 
    body: { id: 123, name: 'Sam' } 
  });

When Contract Testing Helps

Particularly valuable when:

  • Multiple services depend on the same provider. One change can break many consumers.

  • Services are deployed independently. No shared CI run guarantees compatibility.

  • Teams own different services. Coordination friction is high.

  • The system has too many services for end-to-end tests.

Less valuable when:

  • You have a monolith or just a few services. Direct integration testing is fine.

  • One team owns the whole stack. Coordination cost is low; direct testing works.

  • Contracts change rarely. The maintenance cost may exceed the benefit.

Consumer-Driven vs. Provider-Driven

Two flavors:

Consumer-driven contracts: the consumer specifies what it needs. The provider verifies it can deliver.

Provider-driven contracts: the provider specifies what it offers. Consumers verify they can handle it.

Consumer-driven is more common. The reasoning: the consumer's needs drive what the provider must deliver. Providers don't get to ship breaking changes consumers can't handle.

What to Put in the Contract

Contracts should specify:

  • HTTP method, path, query parameters

  • Request headers (auth, content-type)

  • Request body shape

  • Response status code

  • Response headers

  • Response body shape and types

What contracts shouldn't specify:

  • Specific values (use types and presence checks)

  • Internal implementation details

  • Performance characteristics (cover separately)

The contract is the interface, not the implementation.

The Pact Broker

A Pact Broker (or equivalent) is the shared infrastructure for contract testing:

  • Stores pacts published by consumers

  • Provides pacts to providers for verification

  • Tracks which versions are compatible

  • Powers "can-i-deploy" checks

The broker becomes the source of truth for service compatibility. Before deploying, check the broker: are my current versions compatible with what's deployed?

Verification Run Locations

Contracts can be verified:

  • In consumer CI: A's tests generate the pact every time

  • In provider CI: B's tests verify the pacts every time

  • On contract changes: when a pact changes, the affected provider verifies

The "can-i-deploy" check in deployment pipelines ties this together: a service deploys only if its contracts are satisfied.

What Goes Wrong

Contract drift. Provider changes behavior in a way that doesn't violate the contract but breaks the consumer anyway. The contract was too loose.

Mock divergence. Consumer's mocks don't match reality. Tests pass with mocks; production fails.

Stale pacts. Old consumer contracts not retired. Provider verifies against contracts no real consumer uses.

Over-specified contracts. Consumer pins details that aren't really requirements. Limits provider's flexibility for no benefit.

Under-specified contracts. Contract doesn't capture the actual usage. Provider can change things the contract didn't pin.

The contract has to capture what the consumer actually depends on, not more, not less.

Tools

Common contract testing tools:

  • Pact (most popular, multi-language)

  • Spring Cloud Contract (Java/Kotlin)

  • Postman / OpenAPI contracts (lighter weight)

  • OpenAPI / Swagger for schema-level contracts

Pact is the most mature for consumer-driven contracts. OpenAPI is good for documentation-based contracts that may be lighter-touch.

Schema-Level Contracts

A lighter form: just use OpenAPI/JSON Schema to define the API shape, verify both consumers and providers against the schema.

  • Producer publishes OpenAPI spec

  • Consumers generate clients from it

  • Tests verify the producer matches the spec

Less rigorous than full contract testing (doesn't capture consumer-specific expectations), but simpler. For many teams, this is enough.

Versioning

Contracts evolve. Versioning is essential.

A working pattern:

  • Each consumer pact is tagged with the consumer version

  • Providers verify against pacts from all currently-deployed consumer versions

  • Old pacts are retired when their consumer versions are no longer deployed

Without versioning, the provider doesn't know which contracts still matter.

Trade-Offs

Contract testing has costs:

  • Setup overhead (broker, tooling, training)

  • Maintenance of contracts as APIs evolve

  • Discipline to keep contracts focused on actual dependencies

Benefits:

  • Independent deployment of services

  • Fast feedback when changes would break integration

  • No need for full end-to-end test environments

For mature microservices architectures, the trade is usually worth it. For smaller systems, the overhead may exceed the value.

A Working Implementation

For a team adopting contract testing:

  1. Pick one consumer-provider pair. Start small.

  2. Define the contract in consumer tests.

  3. Set up a broker (Pact Broker is free for small use).

  4. Have provider verify in its CI.

  5. Expand to other pairs as the pattern is proven.

Don't try to retrofit contracts onto every service at once. Adoption is gradual.

When to Use End-to-End Tests Instead

Contract testing complements but doesn't replace end-to-end tests entirely. E2E still has value for:

  • The most critical user journeys

  • Verifying configuration and deployment are correct

  • Catching issues that span the contract boundary

The right mix: contract testing for service-pair integrations, E2E for system-level verification of critical flows.

Key Takeaway

Contract testing verifies that services can talk to each other without running them together. Consumers declare what they expect; providers verify they deliver. Particularly valuable in mature microservices architectures where end-to-end testing has become impractical. Use a contract broker to coordinate; version contracts to evolve safely. Keep contracts focused on actual dependencies — over- or under-specifying both cause problems. The combination of contract testing and selective E2E is the working approach for most multi-service systems.

Related reading

Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.

bottom of page