# Contract testing without the tests

Cross-team integrations break at the seams. Each service passes its own tests, and then the two stop agreeing. Contract testing is the usual answer, and most rollouts stall before every consumer has written its tests.

We built [Graphwake](https://graphwake.com/?utm_source=hashnode&utm_medium=article&utm_campaign=contract-testing) around a smaller ask: the OpenAPI spec you already publish, plus one subscription from each team that calls an endpoint. That catches the common breaking changes on every endpoint. It doesn't replace tests where behaviour matters, and we'll come back to where those still pay off.

## Why adoption stalls

Pact describes itself as "a code-first tool for testing HTTP and message integrations using contract tests", and its contract "is generated during the execution of the automated consumer tests" ([docs.pact.io](https://docs.pact.io/), as of 2026-11-14). That's where the cost comes from, and the people who pay it aren't the people who change the API.

The consumer pays. A team that calls ten internal APIs is asked to write and keep contract tests for ten services it doesn't own, and each test only pays off when some other team changes something. That's hard to get into a sprint.

Ownership is split. The consumer writes the contract, the provider verifies it, and someone runs the broker. When a verification fails at 6 pm, it isn't obvious whose job it is. Tests nobody owns rot, and a rotting contract test does real harm because it teaches people to ignore red.

Coordination costs the most. Every pair of teams has to agree on provider states, versions and the order of rollout. Pact's own page on [what it is good for](https://docs.pact.io/getting_started/what_is_pact_good_for) lists "testing providers where the consumer and provider teams do not have good communication channels" as a poor fit. In a company with dozens of services, that can describe a fair share of the pairs.

And coverage stops at the first team that says no. The same page lists an API whose other side "will not also be using Pact" as a poor fit. If the mobile team or a contractor team has no tests, the provider is back to guessing about the consumers it knows least about. Pact's docs are candid about it: "in the wrong situation, it will probably be just as painful!" A company-wide rollout plan tends to assume every pair of teams is in the right situation.

Spec-driven tools such as [Specmatic](https://specmatic.io/) generate the tests from the provider's spec instead (as of 2026-11-14). The contract is then the provider's description of the API, but the spec by itself still doesn't say which teams rely on which part of it.

## What the spec alone can tell you

If you publish an OpenAPI spec and keep it accurate, much of the contract is already written down. Compare the spec before and after a change, and the changes that can make an existing client fail show up without anyone writing a test:

- an endpoint or a response field removed
- a required parameter or a required request field added
- a field's type changed
- the accepted values narrowed, for example a shorter enum

![Example OpenAPI diff of GET /orders/{id}: the currency query parameter changes from required false to required true, and the total response field is removed. Both are flagged as breaking, and the endpoint reaches the checkout and mobile-app teams.](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/nqoh2jd9prukutym5u0y.png)

*Example: two breaking changes found from the spec alone, with no test code.*

That covers the shape of most breaking changes between internal teams. The check runs on every pull request that touches the spec, and the consuming teams don't have to do anything for it.

## What the spec can't tell you

Behaviour, for one. A `total` that switches from dollars to cents is the same `number` in the spec. You only see that by running the service.

Drift. If the spec and the code disagree, a spec check is checking the wrong thing. Generating the spec from the code in the build helps, but it doesn't prove what the service returns.

What a consumer relies on beyond the spec: an undocumented field, the order of a list, an error message someone parses with a regular expression. None of that is in the spec.

And who calls the endpoint. A spec check can say "`total` was removed from `GET /orders/{id}`", but not which teams read `total`, or even which teams call `GET /orders/{id}`. So the finding goes to the pull request author, who usually doesn't know either.

The first three are reasons to keep contract tests where they earn their cost. The last one is why most breaking changes still surprise someone. The diff found the change, and the news went to the wrong person.

## The smallest contract

A consumer-driven contract holds two kinds of information. Most of it is detail: the exact fields, values and states this consumer depends on. The rest is a single fact, that this team calls this endpoint. Only some integrations need the detail, which is expensive to write. Every integration needs the fact, and it costs almost nothing.

So in Graphwake we ask every consumer team for the fact and nothing more. A team that marks "we call `GET /orders/{id}`" has written the smallest contract there is: one subscription, with no test code and nothing to verify. Put that next to the spec diff and you get most of what teams wanted from contract testing. When the spec changes in a breaking way, the teams that call the endpoint hear about it, and with a CI step they hear while the change is still a pull request.

![Example consumer map for GET /orders/{id} in orders-api: owned by payments-team, consumed by checkout and mobile-app, with storefront suggested by Discovery. GET /orders/{id}/invoice has no subscriber and is shown as a blind spot.](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cgl86ob1ii9twkni2l3o.png)

*Example: the subscriptions behind an endpoint, and an endpoint nobody has claimed yet.*

How that works in practice:

- Consumer teams subscribe to the endpoints they call. That's all we ask of them. They write no test code and run no broker. Subscriptions are per endpoint, not per field.
- Graphwake reads only your OpenAPI spec, from a URL it checks every 30 minutes or from one CI step on each pull request. It never asks for access to your code.
- A removed field, a new required parameter, a changed type or narrowed values reaches only the teams subscribed to that endpoint, in Slack, Microsoft Teams or email. Additions stay quiet by default.
- Each change set gets an [impact score from 0 to 100 with public rules](https://graphwake.com/impact-score). A breaking change on an endpoint nobody subscribes to counts as a blind spot, and rule-based Discovery suggests teams that may call it.

The spec plus a list of who calls what also covers every endpoint from the first day, not only the ones whose consumers volunteered to write tests.

## Where contract tests still earn their cost

Keep contract tests where both sides are teams that talk and will maintain them, where a runtime break would be costly (contract tests check the running service without deploying everything), and where consumers depend on behaviour or fields the spec doesn't describe.

The split we suggest: the spec and subscriptions on every endpoint, so every breaking change reaches the teams that call it, and contract tests on the integrations where a runtime break would be costly. If you already run Pact or Specmatic, keep them. Graphwake reads the spec and tells the teams subscribed to a changed endpoint; contract tests check a running service. They work side by side.

---

*Originally published on [graphwake.com](https://graphwake.com/guides/contract-testing-without-the-tests).*

Did your team roll out contract testing across services? How far did it get before it stalled, or did it stick?

