Lesson 1 of 3 · Contract Testing

Contract Testing Fundamentals

A contract is the agreement between a consumer (the service that calls an API) and a provider (the service that serves it). Contract testing verifies that this agreement is kept — on both sides — without running the actual services together.

Contract Testing CTAL-TA v3.1.2 — Section 3.2.2 ~30 min read · ~65 min with exercises

1 The Hook

An Auckland fintech has 8 microservices. The payments service calls the accounts service to validate account numbers before processing a payment. A developer on the accounts team renames the accountNumber field to account_number (snake_case) as part of an internal refactor. Unit tests pass. Integration tests don’t exist. The change merges on Thursday.

Friday afternoon, the payments service starts returning 500 errors for every new payment. The accounts service change was entirely internal — no breaking tests in their suite. It took 3 hours to diagnose: the payments service was still requesting accountNumber (camelCase) from the response, getting undefined, and failing validation.

Contract testing would have caught this at the pull request. The payments service’s consumer test expects a field named accountNumber. The accounts team’s provider verification runs against that contract. When accountNumber is renamed, the verification fails — in the accounts team’s own CI pipeline, before anything merges.

2 The Rule

A consumer-driven contract is a test that says “I (the consumer) expect you (the provider) to return X.” The provider must prove it can satisfy every consumer’s expectations before deploying. Breaking changes are caught in the provider’s own build — not in production.

3 The Analogy

Analogy

A formal service level agreement between two businesses.

The customer writes down exactly what they expect from the supplier. The supplier signs and must prove they can deliver. If the supplier changes their product, they check the agreement first. Nobody finds out about the change by calling the helpdesk on a Friday afternoon.

In the fintech example, the payments team is the customer. The accounts team is the supplier. The PACT contract is the SLA. When the accounts team renamed their field, they would have checked the SLA — seen the payments service expects accountNumber — and known not to rename it, or to update the consumer first. Instead, there was no SLA, and the helpdesk call happened at 2pm on a Friday.

4 Watch Me Do It

A full PACT example: the payments service (consumer) calling the accounts service (provider). Two parts: the consumer test, and the provider verification.

Part 1: Consumer test (payments-service)

The consumer test defines the contract. It says: when I send this request, I expect this response. PACT runs a mock server so the accounts service doesn’t need to be running.

// payments-service/tests/accounts-contract.test.js const { PactV3, MatchersV3 } = require('@pact-foundation/pact'); const provider = new PactV3({ consumer: 'PaymentsService', provider: 'AccountsService', dir: './pacts', // PACT writes the contract file here }); describe('AccountsService API', () => { it('returns account details for a valid account number', async () => { await provider .given('account ACC-1234 exists') // provider state — sets up test data .uponReceiving('a request for account details') .withRequest({ method: 'GET', path: '/accounts/ACC-1234', headers: { Authorization: MatchersV3.regex('Bearer .+', 'Bearer test-token') }, }) .willRespondWith({ status: 200, body: { accountNumber: MatchersV3.string('ACC-1234'), // field name is the contract accountHolder: MatchersV3.string('Aroha Williams'), balance: MatchersV3.number(15000.00), currency: MatchersV3.string('NZD'), }, }); await provider.executeTest(async (mockServer) => { const result = await getAccountDetails(mockServer.url, 'ACC-1234'); expect(result.accountNumber).toBe('ACC-1234'); }); }); });

When this runs, PACT generates ./pacts/PaymentsService-AccountsService.json. This is the contract file. It describes exactly what the payments service expects. The accounts team then verifies against it.

Part 2: Provider verification (accounts-service)

The provider runs its own service and verifies it can satisfy the contract. This is what runs in the accounts team’s CI pipeline.

// accounts-service/tests/pact-verify.test.js const { Verifier } = require('@pact-foundation/pact'); describe('AccountsService provider verification', () => { it('validates the consumer contracts', () => { return new Verifier({ provider: 'AccountsService', providerBaseUrl: 'http://localhost:3001', pactUrls: ['./pacts/PaymentsService-AccountsService.json'], stateHandlers: { 'account ACC-1234 exists': async () => { // Set up test data before PACT sends the request await db.createAccount({ id: 'ACC-1234', holder: 'Aroha Williams', balance: 15000 }); }, }, }).verifyProvider(); }); });
The key moment: if the accounts developer renames accountNumber to account_number, the provider verification fails here — in the accounts service’s own test suite. The response body has account_number but the contract expects accountNumber. The PR fails CI. The renaming does not merge. The payments service never knows it happened.

That is the entire value proposition of contract testing in one paragraph.

5 When to Use It

Contract testing is most valuable when:

  • Multiple consumers depend on one provider. A single accounts service called by payments, notifications, and reporting. Each consumer writes its own contract; the provider verifies all of them.
  • Integration tests are slow or flaky. Contract tests run in isolation without a live environment. A full PACT test suite runs in seconds.
  • Deployment coordination is painful. Teams need to know: can I deploy my accounts service change without breaking payments? The PACT Broker’s can-i-deploy command answers this.
  • Services are independently deployable. Contract testing enforces backward-compatible API changes because breaking a consumer’s contract fails the provider’s build.

Contract testing is less valuable in monoliths (no service boundaries to protect) or simple two-service setups where a standard integration test is cheaper to maintain. The overhead of writing and maintaining contracts pays off at scale.

6 Common Mistakes

🚫 “I used to think: contract testing replaces integration testing.”

Actually: They are complementary. Contract tests verify the agreement — the field names, types, and formats. Integration tests verify behaviour with real data at the system level. You need both, but contract tests run 100× faster and catch breaking changes immediately. Use contract tests to protect the interface; use integration tests to validate business logic across the system.

🚫 “I used to think: the provider team writes the contracts.”

Actually: Consumers write the contracts — that’s the “consumer-driven” part. The provider then verifies it can satisfy each consumer’s expectations. Flipping this produces contracts that miss real usage patterns. A provider-written contract describes what the provider wants to return. A consumer-written contract describes what the consumer actually uses — which is often a much smaller subset.

🚫 “I used to think: if the API returns the right field, the contract passes.”

Actually: You must also verify the data type, the format (e.g., ISO date string not Unix timestamp), and the presence of required fields even when they are empty. Use PACT matchers — MatchersV3.string(), MatchersV3.number(), MatchersV3.regex() — not literal values. Literal values make contracts brittle: balance: 15000.00 fails if the test account has any other balance. MatchersV3.number(15000.00) passes for any valid number.

7 Now You Try

📋 Prompt Lab — Write a PACT Consumer Test

A NZ banking app has a notifications-service that calls a customer-service to get contact details for sending payment alerts. Write a PACT consumer test (JavaScript) that defines the contract for GET /customers/{id}/contact. The response should include email (required), mobile (optional), and preferredContact fields. Include at least one PACT matcher.

8 Self-Check

Click each question to reveal the answer.

Q1: What is the difference between consumer-driven and provider-driven contract testing?

In consumer-driven contract testing, the consumer writes the contract describing what it expects from the provider. The provider then verifies it can satisfy those expectations. This ensures contracts reflect real usage. In provider-driven testing, the provider writes the contract describing what it offers. This produces contracts that describe the provider’s full API, not just what consumers actually need — and it misses the case where consumers depend on a specific field name or format the provider thinks is an implementation detail.

Q2: Why should PACT matchers (like string()) be used instead of literal values in consumer tests?

Literal values make contracts brittle. If you write balance: 15000.00, the contract fails the moment the test account has a different balance. MatchersV3.number(15000.00) passes for any valid number — the literal value is used for the mock response, but the matcher defines what is acceptable in the provider verification. Use matchers for types and formats; use literals only when the exact value is part of the contract (e.g., a fixed enum value like currency: "NZD").

Q3: What is a “provider state” in PACT and why is it needed?

A provider state (e.g., given('account ACC-1234 exists')) tells the provider what test data to set up before PACT sends the consumer’s request. Without it, the provider’s real service has no idea whether account ACC-1234 exists in its database when the verification runs. Provider states are handled by state handlers in the provider’s verification setup — they seed the database, create mock data, or set API stubs so the provider’s response matches what the consumer expects. They are a form of test fixture management for the provider side.

9 ISTQB Mapping

CTAL-TA v3.1.2 — Section 3.2.2: Integration testing and interface testing

The CTAL-TA syllabus covers integration testing approaches including interface testing between components. Consumer-driven contract testing is a specific implementation of interface testing where the interface specification is generated by the consumer and verified by the provider. Also relevant: CTAL-TA v3.1.2 Section 5.2 (test doubles) — PACT’s mock server is a form of stub, and provider state handlers are a form of fixture management analogous to test doubles for state. The PACT Foundation documentation is the primary technical reference for implementation patterns.