Contract testing: prevent an API change from breaking its clients
The team that owns an API renames a field in a response. It is a small change, the service’s tests pass and deployment completes without a problem. Three hours later, another team calls to say that their orders have stopped arriving—their application read that field. Nobody did anything wrong. There was simply no record of who relied on what.
What is contract testing?
Contract testing records the expectations between an API provider and its client in a machine-readable contract. The contract describes, for example, the required fields, their types and specific interactions. If a change violates those expectations, the pipeline can flag it before a release.
Here, “client” does not mean a customer. It means another service or application that uses your API—a mobile application, partner integration or another microservice, for example. Think of it as a drawing of a plug and socket: if both parts match the same drawing, they fit together even if nobody has ever physically connected them.
Why ordinary tests may not detect the problem
The API provider has tests of its implementation. The client may have tests against a simulated response, or mock. Both suites can pass while the systems remain incompatible if each side verifies only its own interpretation of the agreement.
A shared integration environment is a useful layer, but it may not provide early feedback. It is often shared by several teams, and an incompatibility may not appear there until a change is complete and ready for release.
Documentation alone may not be enough. It describes the intended interface, but may not capture which fields and behaviours individual clients actually use. A contract records these specific expectations in a form that can be checked automatically.
As the number of services and clients grows, so does the number of agreements that need to be maintained. With microservices, it can therefore be difficult to determine who will be affected by a change without a shared mechanism.
How we address it
Contract testing places the agreement between services in code. When contracts are verified for relevant API changes, an incompatibility may appear before the two systems are connected.
One open-source tool for consumer-driven contracts is Pact. We select a particular tool only after considering the languages in use, the type of integration and how the teams publish and verify contracts.
In a test, the client describes what it expects from the API: I will send this request and need these fields with these types in the response. This creates a contract—a machine-readable record of the client’s actual need, rather than documentation that becomes outdated.
The API provider then verifies its clients’ contracts against the real implementation. If a change violates a recorded expectation, the check can fail in the pipeline of the service preparing that change.
It is important to understand what a contract does not protect. It is not a list of everything the API returns, but a selection of expectations from a particular client. A field can be changed more safely only when contracts cover all relevant clients and public or unknown API consumers have also been considered.
Contract tests are often faster than a test of the entire integrated system because both sides can be verified independently. All services do not need to run at the same time.
What you gain
For clients covered by contracts, you have a better view of which recorded expectations a planned change will affect. The pipeline can therefore flag an incompatibility before a release, rather than after a report from another team or a customer.
In concrete terms:
- an incompatible change may be detected before deployment instead of only after a report from another team or a customer;
- teams do not have to start the entire shared integration environment for every check;
- a contract can be maintained with the tests and its history can be tracked;
- developers modifying the interface can see more clearly which expectations they must preserve.
Contracts know only about the clients and expectations recorded in them. For a public API or unknown clients, compatibility must also be protected through versioning, clear documentation and an agreed process for retiring older versions.
Contract tests do not replace functional API tests. They protect the boundary between services; a functional suite verifies the correctness of the logic behind that boundary, including authorisation matrices, boundary values and error states. Both layers can be integrated into CI/CD so that, according to risk, they alert the team or block deployment.
Next step
Start by mapping the clients of critical APIs and the changes that have affected them in the past. You can review suitable contract boundaries in a no-obligation consultation as part of our API testing service.