Skip to content

Contract testing between services

A schema lists what is allowed; a pact lists what is used.

Predict first

Before you read: predict

ExamHall's exam API, a NestJS service, runs two checks in CI on every merge request: response validation, which checks each endpoint's real responses against the OpenAPI document in that merge request, and Pact verification of the candidate app's pact. In that pact, GET /attempts/a-1 returns deadlineAt matched as an ISO-8601 date-time. MR !5090 sends deadlineAt as epoch milliseconds (1793714400000 for attempt a-1) and changes the OpenAPI document's deadlineAt to type: integer.

Which checks fail on MR !5090?

Pick one

The 30-second version

A schema says what a provider may send; a consumer's contract says what that consumer uses. With Pact, the consumer's tests run its client against a mock provider and record each request with the minimal response it needs. The provider's build replays those requests against the real provider. Deploys consult the recorded results.

A schema lists what is allowed; a pact lists what is used.

How it works

Step 1 of 11

Consumer and provider. The consumer sends the request (a web app, a mobile app, another service); the provider answers it.

Analogy: A type foundry and its licensees (type design)

A type foundry is revising a typeface. Newspapers, a book publisher and an app maker license it, and each set it in its own layouts.

At the foundry In contract testing
each licensee's proof page, set in the glyphs, weights and figures its layouts use a consumer's pact (steps 2–3)
the font's character map, every glyph it has the OpenAPI schema (step 3)
setting every licensee's proof page in the new version before release provider verification in the provider's build (steps 4, 7)
a proof page that says "any old-style figure" rather than one exact glyph drawing a matcher instead of a literal (step 5)
"set in the bold italic" written at the top of a proof page a provider state (step 6)
the foundry's log of which version set which licensee's proof cleanly the results that deploy checks read (step 8)
a licensee comparing its own copy of the font with the foundry's current release Fowler's contract test (step 10)

In real terms: before changing what you send, ask which real consumer's recorded example would no longer pass, and let that run in your own build.

Breaks down when: a proof page is printed and looked at by a person; a pact is compared by a program, and only on the interactions someone wrote.

Contrasting case: a public API with unknown consumers: there are no proof pages to collect, so only the schema and its diff guard the change.

Examples, run for real

4.1 ExamHall EXH-4790: the format the candidate app reads

The candidate app shows each attempt's countdown from deadlineAt. Its consumer test records GET /attempts/a-1 with deadlineAt matched as an ISO-8601 date-time. MR !5090 sends epoch milliseconds and edits the OpenAPI document to match (EXH-4790):

candidate app's consumer test, Pact mock provider: passes · pact written, 1 interaction, deadlineAt matched as an ISO-8601 date-time
exam API at main (deadlineAt 2026-11-03T14:00:00.000Z): Pact verification passes · response validation passes
MR !5090 (deadlineAt 1793714400000): Pact verification fails at $.deadlineAt · response validation against the MR's document passes
MR !5090's response read by the candidate app's code: throws (deadlineAt is not an ISO-8601 string)

The pact failed the exam API's build at MR !5090, before any candidate's countdown read the number. The document check passed because the document moved with the code.

4.2 Chat: the mock provider is strict about requests

The web app's consumer test declares one request, a message post with the body { "text": "Ship it" }. A refactor makes its client send a clientNonce too:

client sends { text }: the mock answers 201 · consumer test passes · pact written
client sends { text, clientNonce }: the mock answers 500 · consumer test fails: $: Expected a Map with keys [text] but received one with keys [clientNonce, text]

The consumer learns on its own machine that it sends something the pact does not declare; the provider never ran.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A chat API's build verifies the pacts of its two consumers. The web app's consumer test calls its room header code, so its pact records GET /rooms/r-7 returning id, title and lastMessageAt; the web app's pin badge reads pinnedCount, but no consumer test calls it. The mobile app's pact records the same request returning id, title and pinnedCount (matched by type), and the mobile app shows 0 pins when pinnedCount is missing. A merge request stops sending pinnedCount.

Which pact verifications fail in the merge request's build?

Pick one

Question 2 of 7

The mobile app's consumer test declares this interaction:

In the chat API's build, the handler for the state 'room r-7 exists' creates room r-7, titled Design sync, with its last message at 2026-10-21T13:02:00Z. The API's room response also carries archived, unreadCount and pinnedCount. Verification of this interaction fails.

Which change to the consumer test makes it pass?

Code for this question
provider  .given('room r-7 exists')  .uponReceiving('a request for room r-7')  .withRequest({ method: 'GET', path: '/rooms/r-7' })  .willRespondWith({ status: 200, body: {    id: 'r-7',    title: like('Release planning'),    lastMessageAt: '2026-10-20T08:15:00Z',  } });
Pick one

Question 3 of 7

This item runs on this lesson's model of can-i-deploy's rule; no broker runs. The chat API's builds record every verification of its consumers' pacts. Production runs web app 13, mobile app 8 and chat-api 31. The recorded results for two new chat-api versions: web 13 passed against 32 and 33; web 14 passed against 32 and 33; mobile 8 failed against 32 and passed against 33; mobile 9 passed against 32 and failed against 33.

Which chat-api versions may deploy to production?

Pick one

Question 4 of 7

Two checks guard a chat product's link previews. (1) The chat API's build verifies the web app's pact. (2) The web app's own tests use a stub of an outside link-preview vendor, and a separate job sends the vendor's test instance the stub's requests and compares the answers. The vendor changes its API when it chooses.

When should each check run, and what should a failure do?

Pick one

Question 5 of 7

An interviewer asks you to compare two checks. Write 4–6 sentences, with one chat API example of something each catches that the other misses.

Consumer-driven contracts against OpenAPI schema validation: what does each catch?

Question 6 of 7

The mobile app's pact has one interaction: given 'room r-9 is archived', GET /rooms/r-9 returns { "id": "r-9", "archived": true }. In the chat API's build, the verification setup empties the test database before each interaction and defines one state handler, for 'room r-7 exists'. Verification of the interaction fails with status 404.

Which change makes it pass?

Pick one

Question 7 of 7

This item runs on this lesson's model of can-i-deploy's rule; no broker runs. Mobile app 10 adds a call to GET /rooms/{id}/pins, an endpoint chat-api 34 adds. Production runs chat-api 33, web app 14 and mobile app 9. The recorded verifications: mobile 10 failed against chat-api 33 and passed against 34; web 14 and mobile 9 passed against 34.

In which order can the two releases reach production?

Pick one

Go deeper

Pro tips
  1. Keep random data out of your pacts. When: writing consumer tests whose pacts a broker stores (steps 2, 8). A pact identical to one already verified reuses that result, so the consumer need not wait for the provider's build. Verify (RAN, tips.mjs):
    tip 1 two runs, example id "m-9" both times: pact files byte-identical
    tip 1 two runs, example id "m-4821" then "m-9930": pact files different, so a broker sees a new contract
    Source (cited, Pact docs, Matching): "If a new pact is published that is exactly the same as a previous version that has already been verified, the existing verification results will be applied to the new pact publication."
  2. Ask how a request fails, not why. When: a consumer test covers the provider's validation (step 11). Record that a bad request gets the provider's error shape, not each business rule behind it. Source (cited, Pact docs, Contract Tests vs Functional Tests): "In our example of validation, write scenarios about how the validation fails, not why the validation fails."
Source

docs.pact.io timed out from this machine twice on 2026-10-17 (connection timeouts to its Netlify addresses; pact.io itself answered). The Pact pages were therefore read from the site's own source repository, pact-foundation/docs.pact.io on GitHub, at commit 76d06a6f246693e92a417c01d0c6a511887dfc1c (its latest on 2026-10-17; the repository's website/docusaurus.config.js names https://docs.pact.io as its URL), as the PagerDuty, ESLint and NestJS docs were read in earlier packs. Valid as of 2026-10-17.

  • Martin Fowler, ContractTest (bliki, 12 January 2011, retitled 2018; the row's second source), https://martinfowler.com/bliki/ContractTest.html (fowler-contracttest.txt). "These check that all the calls against your test doubles return the same results as a call to the external service would." (step 10).
  • Pact docs, Introduction (the row's first source), https://docs.pact.io/ (pact-introduction.txt). Supports steps 3 and 9: "Unlike a schema or specification (eg. OAS), which is a static artefact that describes all possible states of a resource, a Pact contract is enforced by executing a collection of test cases, each of which describes a single concrete request/response pair"; "On its own, however, it does not provide any test based assurance that the consumers are calling the provider in the correct manner".
  • Pact docs, How Pact works, https://docs.pact.io/getting_started/how_pact_works (pact-how-pact-works.txt). Supports steps 2, 4 and 6: "Provider verification passes if each request generates a response that contains at least the data described in the minimal expected response."
  • Pact docs, Matching, https://docs.pact.io/getting_started/matching (pact-matching.txt).
  • Pact docs, Can I Deploy, https://docs.pact.io/pact_broker/can_i_deploy (pact-can-i-deploy.txt). Supports step 8: "making sure that there is a successful verification result between the version that is about to be deployed, and all the versions of the integrated applications that are already in that environment."
  • Pact docs, Contract Tests vs Functional Tests, https://docs.pact.io/consumer/contract_tests_not_functional_tests (pact-contract-vs-functional.txt).
  • Tools, not sources: Pact JS 17.1.4 and pact-core 20.2.0 (npm, Pact Foundation), ajv 8.20.0, openapi-diff 0.24.1 (npm, Atlassian), whose output the scripts grade as executed fact about those versions.

Checked against the tools and docs as of 2026-10-17.