Backend engineer path · Data and tests · Topic 14 of 18

Contract tests between services

Not started

A contract test checks that two services agree on the messages between them, without deploying both at once. With consumer-driven contracts, each consumer writes down the requests it sends and the parts of the response it actually uses. The provider then proves, in its own build, that it still meets every one of those contracts. A breaking change fails a build instead of reaching production.

Why it matters: End-to-end tests across many services are slow and flaky, and stubs of other teams' APIs go stale. Contract tests catch breaking changes before either side deploys.

  1. 1Consumer-driven

    Each consumer records the requests it makes and the parts of the responses it reads. The provider's contract is the union of those needs.

  2. 2Two halves

    The consumer tests against a mock provider and produces a contract. The provider replays that contract against its real code.

  3. 3Provider states

    Each interaction names the state it needs ("order 42 exists"), and the provider sets that up before the replay.

  4. 4Gate the deploy

    A broker stores contracts and verification results, and answers "can I deploy this version to this environment?"

1

How it works

The explanation, with examples you can run.

Only what is used

The idea comes from Ian Robinson's 2006 article: a provider's real obligations are the combined expectations of its current consumers, not every field it happens to return. A toy verifier makes the consequence clear:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

Dropping a field nobody reads is safe, and the provider learns that without asking. The rename breaks exactly one consumer, and the failure names it.

The Pact flow

Pact is a widely used open-source tool for this, with libraries for most languages. On the consumer side, a unit test describes an interaction against a Pact mock server, and your real client code calls it:

import { Matchers, Pact } from "@pact-foundation/pact"import { getOrder } from "../src/orders-client"const { like } = Matchers const provider = new Pact({ consumer: "checkout-web", provider: "orders-api" }) test("reads an order's status", () =>  provider    .addInteraction()    .given("order 42 exists")    .uponReceiving("a request for order 42")    .withRequest("GET", "/orders/42", (b) => b.headers({ Accept: "application/json" }))    .willRespondWith(200, (b) => b.jsonBody({ id: like(42), status: like("paid") }))    .executeTest(async (mockServer) => {      const order = await getOrder(mockServer.url, 42)      expect(order.status).toBe("paid")    }))

like matches by type, so the contract says "a number" and "a string", not those exact values. A passing run writes a pact file, which CI publishes to a Pact Broker.

On the provider side, a verification test fetches the pacts, starts the real service, sets up each provider state through a handler you write ("order 42 exists" inserts that order), replays every request and compares the responses with what each consumer expects. The results go back to the broker.

Before deploying either side, the pipeline asks the broker:

pact-broker can-i-deploy --pacticipant checkout-web --version "$GIT_SHA" --to-environment production# ...deploy...pact-broker record-deployment --pacticipant checkout-web --version "$GIT_SHA" --environment production

What contracts don't do

A contract test checks the shape of the conversation, not the provider's business rules. Whether refunds are calculated correctly is the provider's own functional tests' job. Keep contracts minimal: put in a field only if the consumer really reads it, or the provider can't change anything. If a provider can't run consumer contracts, checking both sides against a shared OpenAPI schema is a weaker alternative.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.The provider removes a response field that no consumer's contract mentions. What happens at verification?
  2. 2.What is a provider state?
  3. 3.What question does can-i-deploy answer?

0 of 3 right so far. Get all three to finish this topic.