How to do contract testing for microservices
By qtrl Team · Engineering
The integration test that catches a broken API contract usually doesn't exist, and the reason is structural. Unit tests mock the other service, so they pass no matter what that service does. End-to-end tests would catch it, but they need every service running, so teams run a handful of them and only on the main flows. The gap in between is where a renamed field ships on a Tuesday and takes down a consumer on Thursday.
Contract testing fills that gap. It's narrower than most people expect, which is exactly why it works.
The idea in one paragraph
A consumer declares what it needs from a provider: these endpoints, these fields, these types. That declaration is the contract. The consumer's tests run against a mock built from it, and the provider's pipeline runs a separate check proving its real implementation satisfies every contract its consumers published. Neither side needs the other running. Both sides find out immediately when the shape stops matching.
Note what it doesn't verify: business logic, data correctness, performance, or whether the feature does anything useful. It verifies that the interface both sides agreed on is still the interface both sides implement. That's a small claim, and it happens to cover the majority of what breaks between services.
Consumer-driven, and why that direction
The contract is written by the consumer, not the provider. That feels backwards the first time and it's the point of the whole approach.
A provider-written contract describes everything the API can do. A consumer-written contract describes what one particular client actually uses, which is usually a small fraction of it. That difference has a practical payoff: the provider can change any field nobody consumes, freely, with the tests confirming nobody consumes it. Without that, every API change is a negotiation with an unknown number of teams.
Pact is the established tool here, with documentation covering most languages. Spring Cloud Contract is the common choice in JVM shops. For an OpenAPI-first organization there are lighter options that diff a spec against an implementation, which gets you part of the benefit with much less setup.
Setting it up
Start with one pair. Pick the integration that has broken most often. Not the most important one, the most fragile one. The first contract is where you work out the conventions, and you want that learning to pay for itself immediately.
Write the consumer expectation. In the consumer's test suite, describe the interaction: given this provider state, when I send this request, I expect a response with these fields and types. Be specific about types and loose about values. Asserting an ID is a string is a contract. Asserting it equals 42 is a brittle test pretending to be one.
Publish the generated contract. Running the consumer tests produces a contract file. It needs to land somewhere the provider's pipeline can read it, either a broker or a repository, and it needs to be versioned against the consumer version that produced it.
Verify on the provider side. The provider's CI fetches the contracts and replays each interaction against the real implementation. Any contract that fails, fails the build. This is the step that makes the whole thing worth doing, and it's the step teams skip when they run out of enthusiasm.
Provider states are where it gets fiddly
Most interactions assume something about the provider's data. "A user with ID 123 exists." "The order is in shipped status." The provider has to be able to put itself into that state before replaying the interaction.
Keep the set of states small and name them clearly. Every distinct state is setup code someone maintains, and a proliferation of near-identical states is the main reason contract test suites get abandoned. Reuse aggressively. Ten well-named states covering forty interactions is healthy. Forty states is a warning sign.
The setup itself should go through the application's own code paths where possible, for the same reason test data is better created through the API than injected into the database. Data written directly can be in a shape the application would never actually produce.
Where it fits against everything else
| Layer | Catches | Misses |
|---|---|---|
| Unit tests | Logic errors inside one service | Anything about the boundary |
| Contract tests | Interface drift between services | Whether the data is correct |
| API tests | Behavior of one service end to end | Multi-service journeys |
| End-to-end tests | The user journey working for real | Cheap, fast feedback |
Contract tests don't replace your API tests. They let you cut the number of end-to-end tests that exist purely to confirm two services still speak the same language, which tends to be a meaningful share of a slow E2E suite.
The organizational part
The technical setup takes a couple of days. The hard part is that contract testing requires two teams to agree on a shared artifact and both to keep their side of the pipeline honest.
It fails in a predictable way: the provider team starts ignoring failing verifications because the contract came from a consumer team they don't talk to. Once a red verification is routine, the whole mechanism is decorative. If you can't get agreement that a failed contract verification blocks a release, contract testing will not survive its first quarter, and you're better off putting the effort into API tests you control end to end.
Worth it when
You have more than a handful of services, owned by more than one team, deployed independently. Below that, the coordination cost outweighs the benefit and an integration test against a real instance is simpler.
Above it, the calculation flips hard. Every service you add multiplies the number of pairs that can drift, and end-to-end coverage of all of them gets impractical fast. Contract testing is the thing that keeps integration confidence from degrading as the service count grows.
qtrl handles the layer above: the user journeys that cross several services and only make sense from the outside. Contract tests confirm the pieces still fit. Browser-level runs confirm the assembled product does what a customer expects.
Because coverage is tracked as structured cases rather than a pile of scripts, you can see which journeys are covered end to end and prune the E2E tests that exist only to catch interface drift once contracts are doing that job. See how it works.
Have more questions about AI testing and QA? Check out our FAQ