Contract Testing

Contract testing checks an integration point by testing each side on its own against a shared contract: the requests a consumer sends and the responses it expects from a provider. The consumer calls (a mobile app, another service); the provider answers (an API). Neither test needs the other side running, so each team verifies its part in its own pipeline.

Example: the mobile app reads id, status and total from GET /orders/{orderId} of the Orders API. The app is tested against a mock that answers as the contract says; the Orders API is tested by replaying the request and checking that its real response still fits. If a developer renames total, the provider's check fails in its own pipeline, long before the two meet in a shared environment. A contract is either consumer-driven or specification-based, taken from an OpenAPI document.

Why it matters

Services and apps are released independently. A provider change can pass all its own tests and still break a consumer, because those tests do not know what the consumer reads. Without contract tests the break appears only when both run together: days later in an end-to-end environment, or in production, where old mobile app versions keep calling the API.

For the tester, contract tests answer a precise release question: will this provider version work with every consumer version it will meet? When it will not, they name the consumer, the interaction and the field.

Consumers, providers and the contract

Martin Fowler describes a contract test as a check that a test double, the stub or mock that other tests use in place of a service, still behaves like the real service. Contract testing extends this to both sides: each is tested against the same written contract.

For an HTTP API, each interaction in a contract states:

  • The request: method, path, query parameters, the headers that matter, and the body.
  • The expected response: status code, headers, and the body fields with their types or patterns.
  • The provider state: the data that must exist for the interaction to make sense, such as "order ORD-1042 exists".

Consumer-driven contract testing

In consumer-driven contract testing the consumers write the contract through their own tests, so the provider knows exactly which fields each consumer uses and can change the rest freely. The cost: every consumer team writes and maintains these tests and the provider runs them, which suits services within one organisation, not a public API with unknown consumers.

Pact, an open-source tool for this approach, works in five steps (its contract file is shown below):

  • Consumer test: the consumer's client code calls a mock provider that answers as the test declares, proving the consumer handles that response.
  • Contract file: the run writes each interaction into a pact: the requests the consumer makes and the response fields its test expects, which should be only the fields it reads.
  • Publish: a broker stores the contract with the consumer's version.
  • Provider verification: the provider's pipeline sets up each provider state, replays each request against the real provider and compares the response with the expected one, by type or pattern where exact values do not matter; extra fields are allowed by default.
  • Deployment check: before either side deploys, the broker confirms that this version was verified against the versions in the target environment (can-i-deploy in Pact).
Consumer-driven contract testing for the Orders API: the mobile app’s test against a mock writes the contract, a broker shares it, and the Orders API pipeline replays it before a deployment check.
  1. Consumer pipeline: mobile-app

    Tested against a mock, never against the real Orders API

    1. 1Consumer testClient code calls a mock provider that answers as declared
    2. 2Contract writtenOnly the requests made and the fields readGET /orders/ORD-1042 → 200 {id, status, total}
    3. 3Published to a brokerStored with the consumer version
  2. Provider pipeline: orders-api

    Verified against the contract, without the app

    1. 4Provider state set upThe test data the interaction needsorder ORD-1042 exists and is shipped
    2. 5Request replayed, response matchedAgainst the real provider, by type or pattern

      Branch: total renamed → $.total missing: verification failsFail

    3. 6Deployment checkcan-i-deployOnly versions verified against each other are releasedGate

Names from Pact: the contract file is a pact and the check is can-i-deploy; other tools name the same steps differently.

{
  "consumer": { "name": "mobile-app" },
  "provider": { "name": "orders-api" },
  "interactions": [
    {
      "description": "a request for an existing order",
      "providerStates": [{ "name": "order ORD-1042 exists and is shipped" }],
      "request": { "method": "GET", "path": "/orders/ORD-1042" },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": { "id": "ORD-1042", "status": "shipped", "total": 149.5 },
        "matchingRules": {
          "body": {
            "$.id": { "matchers": [{ "match": "regex", "regex": "^ORD-[0-9]+$" }] },
            "$.total": { "matchers": [{ "match": "type" }] }
          }
        }
      }
    }
  ],
  "metadata": { "pactSpecification": { "version": "3.0.0" } }
}

Try it: Response Contract Comparator

Specification-based contract testing with OpenAPI

Here the provider's OpenAPI document is the contract; some teams call this provider-driven contract testing. The check from the API schema validation lesson runs on every response in the provider's tests, errors included, and consumers build their mocks from the same document.

Its blind spot: if code and document change together, the provider still matches its own document. So each new version of the document is also compared with the production version, and breaking changes are flagged:

  • Breaking: removing or renaming an operation or a response field, changing a type, making an optional request field required, rejecting request values that were accepted before.
  • Usually safe: adding an operation, an optional request field or a response field, if consumers ignore unknown fields.
  • Needs a decision: a new enum value or null in a response; a client that handles only the known values can fail.
  • The limit: the document says what the provider offers, not which consumer uses what, so a "breaking" change may affect nobody.

Try it: API Contract Validator Try it: OpenAPI Validator

Worked example: a field rename in the Orders API

The Orders API has two consumers with contracts: the mobile app reads id, status and total; the delivery service reads id, status and deliveryCity. A release renames total to grandTotal, turns createdAt from a text date into a number and adds an optional giftMessage. The OpenAPI document was updated too, so validation against it passes.

  • Comparing the two documents flags two breaking changes: total removed and createdAt retyped.
  • Provider verification fails for the mobile app at $.total; the delivery service passes, because it uses neither field.
  • No contract reads createdAt, so the type change is safe for both known consumers, though not for consumers outside the broker.
  • The deployment check blocks the release. The team returns both total and grandTotal, marks total as deprecated, and removes it when no supported app version needs it.
  • An end-to-end run would have shown the rename only after both services were deployed, as an order screen without an amount.

Try it: JSON Diff Try it: OpenAPI Viewer

What contract tests catch compared with end-to-end tests

An end-to-end test drives a journey through every real service, so it finds an incompatible interface only after all services are deployed together, it is slow, and its failure must be traced through many interfaces. A contract test checks one interface in seconds, in each side's own pipeline, and names the consumer, the interaction and the field.

  • Caught before deployment: removed, renamed or retyped fields, changed paths, methods, status codes or headers, and requests the provider no longer accepts.
  • Missed: wrong values, business rules and meaning. A wrongly calculated total, or riyals sent where halalas are expected (the integration testing lesson), still has the right type.
  • Missed: configuration and deployment, such as a wrong base URL, missing credentials or an expired certificate.
  • Missed: anything in no contract, such as an enum value or an error that no provider state produces.

Common mistakes

  • Writing the contract from the provider's full response instead of what the consumer reads, so every harmless provider change fails verification.
  • Matching exact values where only the type matters (generated ids, timestamps), or only the type where the value matters (status codes, enum values).
  • Writing contracts only for success responses; 404, 401, validation errors and empty lists are what clients most often mishandle.
  • Running provider verification without gating deployment on it, so incompatible versions still reach production.

The QA perspective

Contract work starts in refinement: for every API change, ask who consumes the endpoint, which fields they read and whether the change is breaking. A consumer map of the APIs you test answers this and shows missing contracts. Review contracts like test cases: are errors, empty lists and optional or nullable fields covered, and do provider states use realistic data?

In the pipeline, check that contracts are published, that verification runs on every provider change and that the deployment check can stop a release. When verification fails, record both versions, the interaction, the field path and the expected and actual values, then ask whether the change was intended: if it was, plan a coordinated rollout; if not, report a regression in the provider.

Practice exercise

Judge four API changes against two consumers' contracts

Your task

The customers-api team plans four changes to GET /customers/{id}, which loyalty-app and sms-service consume under contracts (see the material). Work out what each approach reports and what the team should do.

Material
customers-api   GET /customers/{id}

Response today (200):
  { "id": "C-77", "name": "Sara Al-Harbi", "phone": "+966500000001",
    "tier": "gold", "createdAt": "2026-03-01T09:30:00Z" }

Consumer contracts (fields each one reads):
  loyalty-app   id (string), name (string), tier ("standard" | "gold")
  sms-service   id (string), phone (string)
  Provider state in both contracts: customer C-77 exists (tier "gold")

Planned changes for the next release:
  1  add an optional field "email" (string)
  2  rename "phone" to "mobile"
  3  allow a new tier value "platinum"
  4  change "createdAt" from an ISO 8601 string to a number (Unix seconds)
Steps
  1. Classify each change as a comparison of the old and new OpenAPI documents would: breaking, usually safe, or needs a decision.
  2. For each change, decide which consumer's verification fails, if any.
  3. Mark where the two answers differ and explain why.
  4. Propose a safe rollout for each change that breaks a consumer.
Show the model answer
Model answer

Change 1 (email): usually safe; both verifications pass, as no contract mentions it and extra response fields are allowed by default. Both approaches agree.

Change 2 (phone to mobile): breaking; sms-service fails at $.phone, loyalty-app passes. Return both fields, mark phone as deprecated, let sms-service switch and publish a new contract, then remove phone once no deployed sms-service version needs it.

Change 3 (platinum): a new enum value in a response needs a decision, and every verification passes because the provider state returns a gold customer. Yet loyalty-app accepts only standard and gold, so the first platinum customer can break it, and neither check shows this. Ask the app team, release a loyalty-app version that handles platinum (or any unknown tier), add a platinum provider state to its contract, and only then let the API return the new value. An end-to-end run shows the problem only if its data has a platinum customer.

Change 4 (createdAt as a number): breaking in the document, yet every verification passes because no contract reads it: safe for these two consumers, breaking for any consumer outside the broker. If such consumers may exist, add the number as a new field and leave createdAt unchanged.

Frequently asked questions

Do I need Pact to do contract testing?

No. Pact is an open-source tool built for consumer-driven contracts, but specification-based contract testing needs only an OpenAPI document and a validator. The principle matters more than the tool: test each side against a shared, versioned contract, and let the result gate deployment.

Who writes consumer-driven contracts, developers or testers?

Usually the consumer's developers write the consumer tests in the consumer's code base, because they are tests of the client code. The provider team owns verification and the provider states. A tester reviews which interactions are covered (errors, empty lists, optional fields), and checks that provider states use realistic data and that the deployment check is in the pipeline.

Do contract tests replace end-to-end tests?

Partly. They replace end-to-end tests whose only purpose is to show that two services still talk to each other. Keep a few end-to-end journeys for critical flows, for configuration and deployment, and for business rules that span services, which contracts cannot see.

Does contract testing work with a third-party API?

Not the consumer-driven form: it needs the provider to run your contract, and a third party such as a payment gateway will not. Use the specification-based form: validate its sandbox responses against its published OpenAPI document, if it has one, and regularly run a few tests against the sandbox to confirm your mocks still match the real service, which is Fowler's original meaning of a contract test.

References

  • Contract Test (Martin Fowler)martinfowler.com · 2011Contract tests: periodic checks that the calls made against a test double return the same results as the same calls to the real external service.
  • Introduction — Pact DocsPact FoundationConsumer-driven contract testing with Pact: each application is tested in isolation against a contract generated while the consumer's tests run.
  • OpenAPI SpecificationOpenAPI InitiativeThe standard for describing REST APIs: paths, operations, parameters, schemas, responses and security schemes.
  • The Practical Test Pyramid (Ham Vocke)martinfowler.com · 2018A widely cited explanation of how to balance unit, integration, contract and UI tests in an automated test portfolio.

Practise with Mutqan tools

Validate that an API request/response pair conforms to its OpenAPI contract: path, method, parameters, body and response schema.

Validation

Compare an actual API response against an expected contract (example or schema) and report breaking vs non-breaking differences.

Validation

Validate OpenAPI 3.x and Swagger 2.0 documents for structural errors and best-practice issues.

Web

Render an OpenAPI 3.x / Swagger 2.0 spec (JSON or YAML) as browsable endpoint documentation.

Web

Compare two JSON documents semantically and highlight added, removed and changed values.

JSON & Data

Go deeper

Your progress on this topic

Part of the QA Roadmap