- Contract testing verifies the whole shape of a response — field names, types, nullability, required properties — rather than the handful of fields a given test happens to read.
- That is precisely why integration tests miss schema drift: they assert on a sample, so a field that changes type without being read still passes.
- Removing or renaming a field, narrowing a type, or making a parameter required are breaking changes. Adding an optional field generally is not.
- Contract failures should block the build, because the alternative is discovering them when a consumer breaks in production.
What is contract testing?
A contract is the agreement between an API and the code that calls it: these fields will be present, they will have these types, this one may be null and this one may not. Contract testing verifies that the running service still honours that agreement — before a change reaches the consumers who depend on it.
It sits between unit testing and integration testing in cost and above both in a specific kind of value: it catches the class of defect where nothing is broken from the provider's point of view, and everything is broken from the consumer's.
Why integration tests miss schema drift
This is the argument for contract testing, and it is worth being precise about.
A typical integration test creates an order and asserts that the response contains an id and a status of "pending". It reads two fields. Now suppose a refactor renames customer_email to customerEmail, or changes total from a number to a string "49.99", or makes shipping_address nullable.
The integration test still passes — it never read those fields. Meanwhile the mobile app that renders the order confirmation crashes on a null address, and the billing service that parses total as a float starts throwing. The provider's test suite is green throughout.
Integration tests assert on a sample of the response. Contract tests assert on its shape. That difference is the entire reason the discipline exists.
What counts as a breaking change
| Change | Breaking? | Why |
|---|---|---|
| Removing a response field | Yes | Consumers reading it get undefined |
| Renaming a field | Yes | Equivalent to removing one and adding another |
| Narrowing a type (number → integer) | Yes | Previously valid values now rejected |
| Making a response field nullable | Yes | Consumers may not handle null |
| Making a request parameter required | Yes | Existing callers omit it |
| Removing an enum value | Yes | Consumers may map on it exhaustively |
| Adding an optional response field | No | Existing consumers ignore it |
| Adding an optional request parameter | No | Defaults preserve current behaviour |
| Adding a new endpoint | No | Nothing depends on it yet |
| Adding an enum value | Sometimes | Breaks consumers with exhaustive switches |
The pattern is that additive change is safe and subtractive change is not. This is why mature APIs accumulate fields rather than reshaping them, and why deprecation is a scheduled process rather than a deletion.
Provider-driven vs consumer-driven contracts
Provider-driven
The provider publishes a schema — typically an OpenAPI document — and tests that its responses validate against it. Simple to set up, and it catches drift between documentation and implementation. Its weakness is that it tells you nothing about which changes actually matter, because it does not know who reads which field.
Consumer-driven
Each consumer publishes the subset of the response it genuinely depends on. The provider verifies it satisfies every published expectation before shipping. This is more work to establish and considerably more useful: it converts "we think this field is unused" into a checkable fact, and makes the blast radius of a change explicit before it ships.
Putting it in the pipeline
- Capture the current schema as a baseline. Generate it from live responses if you have no specification — an inferred schema is far better than none.
- Validate every response against it on each run. Not just the fields the test reads: the entire body.
- Classify differences automatically. Additive changes pass with a note; subtractive changes fail.
- Fail the build on breaking changes. Contract violations are one of the few failure classes that should block a merge unconditionally.
- Version deliberately when you must break. Introduce the new shape alongside the old, migrate consumers, then remove the old one on an announced schedule.
Contract testing and microservices
The value of contract testing scales with the number of services. In a monolith, a rename is caught by the compiler. Across twelve services deployed independently, nothing catches it — the calling service is compiled, tested and deployed against a version of the interface that no longer exists.
This is also why end-to-end environments are a poor substitute. Standing up every service to test one change is slow, expensive and flaky, and it still only exercises the paths the end-to-end test happens to cover. Contract tests give you most of that assurance without the environment.
Test your API without writing the tests
Paste a URL. Flasqo discovers your endpoints, generates the suite and runs it — free, no credit card.
Start testing free