Contract Testing
Contract testing (PACT) verifies that two services agree on the shape and meaning of the data they exchange, before they ever talk to each other in integration tests or production. Catch breaking changes early, in isolation, and in parallel.
1 The Hook
A New Zealand fintech split its monolith into services. The payments team owned one service; the orders team owned another. One Tuesday the payments team tidied up their API response — they renamed the field transactionId to txnId, a harmless-looking cleanup. Their own unit tests all passed. They deployed.
The orders service was still reading transactionId. It started getting null back, marked perfectly good payments as failed, and customers at checkout saw “payment declined” on cards that had actually been charged. Nothing in the payments team's own tests caught it, because from their side the change was internally consistent. Nothing in the orders team's tests caught it either, because they were testing against a mock that still returned the old field name. The two services had quietly drifted apart, and the only place the mismatch surfaced was production.
Contract testing exists to make that drift impossible to merge. The orders team writes down exactly what it needs from the payments service — a contract — and the payments service's own CI checks every change against it. Rename transactionId and the build goes red on the payments side, before anything ships, with a message naming the consumer that depends on it.
2 The Rule
The consumer writes down exactly what it needs from the provider as a contract, and the provider's own pipeline verifies its real code still satisfies every consumer's contract — so a breaking change fails the build before it ships, not in production.
3 The Analogy
The power socket standard, not a soldered wire.
Every appliance in a Kiwi home plugs into the same NZ three-pin socket. The kettle maker and the electrician never meet, never coordinate releases, never test together — yet any kettle works in any socket, because both sides build to a published standard: pin spacing, voltage, the shape of the plug. The standard is the contract. The electrician can rewire the house and the kettle still works, as long as the socket still matches the agreed shape. If someone changed the socket dimensions, every appliance would fail at once — so the standard is exactly what you check against before changing anything.
Contract testing is checking the plug against the socket standard in the factory, before the appliance ever reaches a house. Soldering the kettle's wires straight into the wall — the equivalent of running both services together every time — works once, but you can never change either side without cutting and re-soldering.
Contract testing lets two independently-deployed services verify their API agreement in CI — without ever running together — by having the consumer publish exactly what it needs and the provider prove its real code still satisfies it on every build. Reach for it whenever services are owned by different teams and deploy on separate schedules, especially when you have a provider serving multiple consumers (a field rename that breaks one consumer should fail the build, not production). The most common mistake is writing the contract from the provider's side — contracts must be consumer-driven, because only the consumer knows which fields it actually reads at runtime; a provider-authored contract describes what the API offers, not what breaks when it changes.
From the field
A NZ fintech team integrated with three bank APIs and wrote service tests against each provider. The tests passed for six months until one bank silently changed their error response schema — the field name shifted from errorCode to error_code. The integration broke in production before anyone noticed the contract had changed. After adopting Pact consumer-driven contracts, schema changes became a build failure, not a production incident. Integration testing alone does not protect you from provider drift — contracts do.
What it is
Contract testing is a pattern for verifying that two independent services (a consumer and a provider) can communicate correctly, without needing to deploy them together or mock every variant of their interaction. Instead, the consumer defines what it needs from the provider (a “contract”), and the provider verifies it can satisfy that contract — all in parallel, in CI, long before either service touches production.
The core insight: integration tests are expensive and slow because you have to run both services together. Contract tests split that job. Consumers test in isolation (mocking the provider), providers test in isolation (verifying real code against consumer expectations), and both can run in parallel in CI. You only test the real integration once, briefly, in staging.
Why not just mock? Mocks let the consumer and provider diverge silently. A contract makes the agreement explicit and testable: if the provider changes and breaks the contract, the test fails immediately, before the code is merged.
Why it matters
In a monolith, you have one integration test suite that catches breaking changes. In microservices, you have many services, each with its own deployment schedule. Without contract tests, the only way to catch a breaking change is to deploy both services and run end-to-end tests — slow, expensive, and you don’t know which service is at fault.
Contract tests shift that burden left: the provider knows immediately (in their own CI) whether they have broken any consumer, because each consumer has published its contract. The consumer knows immediately whether the provider has changed in a way that breaks them. Both happen in CI, in parallel, without waiting for integration environments.
Consumer-driven contracts
A contract is consumer-driven: the consumer defines what it needs from the provider, and the provider verifies it can deliver.
- Consumer writes a test that describes what it expects from the provider: “When I POST /orders with {customerId: 123, total: 49.99}, I expect a 201 response with {orderId: ..., status: 'pending'}.”
- The test runs with a mock provider (using PACT or similar), verifies the consumer code works, and generates a contract file (JSON).
- The contract is published to a contract broker (a shared repository).
- The provider pulls the contract from the broker and tests their real code against it: “Does my /orders endpoint actually return what this consumer expects?”
- If the provider breaks the contract, their CI fails immediately. They either fix the code or negotiate a new contract with the consumer.
PACT framework overview
PACT is the most widely used contract testing framework. It works in four steps:
| Step | Who | What happens | Output |
|---|---|---|---|
| 1. Consumer test | Consumer team | Consumer code tests against a PACT mock provider. The test exercises the consumer code: “Call the payment service, expect a 200 and {transactionId: ...}” | Contract JSON file (pact) |
| 2. Publish | Consumer CI | The pact JSON is published to a contract broker (e.g. PactFlow, Pact Broker) | Contract stored in broker |
| 3. Provider verify | Provider team | Provider CI pulls all contracts from the broker and tests the real provider code against each one: “Does my payment service return what all consumers expect?” | Pass/fail per contract |
| 4. Deploy with confidence | Both teams | If all contracts pass, both services can deploy independently. No surprises in production. | Safer deployment |
When to use it
- Microservices with multiple consumers — one provider, many consumers. Each consumer defines its own contract; the provider verifies all of them.
- Third-party API integration — you cannot deploy the third-party service, but you can write consumer contracts against their published API and catch breaking changes in CI.
- Parallel deployment — teams deploy independently. Contract tests ensure you don’t break downstream services without knowing.
- Not suitable for monoliths — in a monolith, you have one integration test suite; contract tests don’t add value. Use contract tests when services are independently deployable.
Contract testing process
Consumer side
The consumer test is a unit test that mocks the provider and verifies the consumer code works correctly:
- Set up a PACT mock provider on a local port.
- Write a test: “When I call the payment service with {amount: 50, currency: 'NZD'}, I expect a 200 with {transactionId: ..., status: 'success'}.”
- The mock provider records what the consumer expects.
- Run the consumer code and verify it works.
- The test framework writes a pact file (JSON) describing the contract.
- Publish the pact file to the contract broker.
Provider side
The provider verification test pulls contracts from the broker and tests the real code:
- Pull all pact files from the contract broker.
- For each pact, extract the requests the consumer expects.
- Send those requests to the real provider code (running locally).
- Verify the responses match the pact exactly (status code, headers, body schema, data types).
- If the real provider breaks the contract, the test fails. The provider team must either fix the code or negotiate with the consumer.
Worked example: Order and Payment services
Imagine an e-commerce system with two services:
- Order Service — creates orders, calls Payment Service to charge the customer.
- Payment Service — processes payments, returns transaction IDs.
{
"consumer": {
"name": "OrderService"
},
"provider": {
"name": "PaymentService"
},
"interactions": [
{
"description": "a valid payment request",
"request": {
"method": "POST",
"path": "/payments",
"body": {
"orderId": "ORD-123",
"amount": 49.99,
"currency": "NZD"
},
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"status": 201,
"body": {
"transactionId": "TXN-456",
"orderId": "ORD-123",
"status": "success",
"amount": 49.99
},
"headers": {
"Content-Type": "application/json"
}
}
}
]
}
The Order Service CI publishes this pact to the contract broker. Then, the Payment Service CI pulls it and tests:
// Payment Service verifies the contract
@Test
public void testOrderServiceContract() {
// PACT framework pulls the contract from broker
// Sends the request: POST /payments with {orderId, amount, currency}
// Gets the real response from Payment Service code
// Compares: response status 201? body has transactionId? amount matches?
// If Payment Service code changed the response to:
// status: 200 (should be 201)
// body: {txId: ...} (consumer expects "transactionId", not "txId")
// Then: TEST FAILS. Provider team must fix.
}
If the Payment Service test passes, the provider team knows they have not broken any consumer. If it fails, they know exactly which consumer expects what, and they must fix it before deploying.
Tools
- PACT — the standard, open-source contract testing framework. Available for Java, JavaScript, Go, Python, .NET, and more.
- PactFlow — managed SaaS contract broker; hosts pact files, integrates with CI/CD, provides visibility.
- Pact Broker — open-source contract broker you can self-host.
- Spring Cloud Contract — Java-specific contract testing, similar to PACT but closer to Spring ecosystem.
- Swagger / OpenAPI — not contract testing per se, but API specs can be verified against contracts.
Common pitfalls and best practices
- Over-mocking the provider — the mock provider should return exactly what the real provider will return. If your mock is too permissive, the contract will pass but the real provider will fail.
- Testing too much behavior in contracts — contracts should specify the API boundary (request/response shape), not internal business logic. Leave complex behaviour to unit tests.
- Forgetting to verify contracts in CI — if the provider doesn’t actually verify contracts in CI, breaking changes will slip through. Make it mandatory.
- Not publishing contracts often enough — publish whenever the consumer changes its expectations, not just on release. This gives the provider feedback in real time.
- Ignoring contract failure — if a provider test fails because they broke a contract, they must fix it. Don’t ignore the failure or bypass the test.
- Treating contracts as the only integration test — contracts verify that the API boundary is correct, but they don’t test end-to-end scenarios. Still run integration tests, but less frequently (after contract tests pass).
Key principle: Contract tests are not a replacement for integration or end-to-end tests. They are a fast, parallel way to catch breaking changes before integration tests run. Think of them as unit tests for the API boundary.
The subtlest contract testing failure I keep seeing: the broker is green, every contract passes, and production is still broken. How? The provider is verifying against main but deploying a feature branch. Or the consumer published a contract from their local machine six months ago and nobody touched it since. The broker shows “passing” because it is checking the wrong version. The fix is not more contracts — it is wiring can-i-deploy into both pipelines so deployment is gated on the specific commit SHA being shipped, not just “whatever last passed”. I have seen three separate NZ government integration projects collapse on exactly this. The tool is fine; the version discipline is what kills you.
4 Industry Reality
- Most teams talk about contract testing but only a minority actually run it in CI. You will join teams where the “pact broker” is a forgotten Docker container that last ran six months ago — your job is to revive it and make provider verification mandatory before it becomes a blocker.
- The hardest part is not the tooling — it is convincing the provider team to treat a consumer contract failure as their problem to fix. In NZ companies with small engineering teams and flat hierarchies this is usually a conversation, not a ticket; in larger orgs you may need to escalate through a platform or API governance team.
- Third-party APIs (e.g. CloudBooks, the Revenue NZ gateway, or LandNZ property data) will never run your pact verifications. Instead, experienced testers write consumer contracts to lock down exactly which fields they depend on, then use provider states to detect drift when the vendor releases a new API version — catching breaking changes in their own CI before a vendor goes live.
- Schema drift is much more common than outright breakage. Fields get a type change (string → number), an optional field silently becomes required, or a date format shifts from
YYYY-MM-DDto epoch millis. Contract tests catch all of these; a hand-rolled mock does not. - In brownfield microservice estates you will find services that have never had contracts written and have been running on undocumented assumptions for years. Start by writing contracts that document the current behaviour as-is, ship them to the broker, then use them to gate future changes — it is far safer than trying to retrofit tests to an agreed spec that does not exist yet.
5 When to Use It — and When Not To
✓ Use it when
- Two or more independently deployable services exchange data over HTTP, gRPC, or a message queue and are owned by different teams or repos.
- The provider has more than one consumer — contract testing tells the provider exactly which consumers depend on which fields, so a rename or removal fails fast in the right place.
- You integrate with an external SaaS API (CloudBooks, Stripe NZ, Revenue NZ) that you cannot stub reliably — write a consumer contract to lock the fields you depend on and detect version drift early.
- Deployment pipelines are independent — the provider team ships at a different cadence from the consumer team and you need a safety net between their releases.
- Your end-to-end test suite is too slow to run on every PR and you need a faster boundary check that can run in parallel with unit tests.
✗ Skip it when
- Your system is a monolith — all code is in one repo, one deploy, one integration test suite. Contract tests add overhead with no benefit here.
- The consumer and provider are owned by the same person or tiny team and change together on every commit. Just write an integration test.
- You only have a single consumer and the provider has no other users — a shared interface contract adds ceremony without protecting anyone new.
- The API is still in rapid flux and the contract schema changes daily. Wait until the API stabilises, then introduce contracts to lock the shape down.
- The team has no CI pipeline capable of running automated tests. Contract tests without CI enforcement are decoration; fix the pipeline problem first.
Context guide
How the right level of contract testing effort changes based on project context.
| Context | Priority | Why |
|---|---|---|
| Benefits NZ or CoverNZ benefits portal calling an independently-deployed eligibility or payment service owned by a different squad | Essential | Services deploy on different schedules; a field rename on the eligibility side silently breaks the portal in production. A contract makes that break a build failure instead. |
| Revenue NZ myIR consumer integrating with the Revenue NZ gateway API or a third-party tax calculation service | Essential | You cannot deploy the provider to run verification, so a consumer contract locked to the specific fields you depend on is the only way to catch API drift before a vendor release reaches production. |
| Harbour Bank or Pacific Bank open-banking API provider serving multiple consumer teams (mobile app, web banking, broker portals) | High | Multiple consumers mean a single field change can break several products simultaneously. The broker surfaces exactly which consumers are affected before the provider merges anything. |
| Pacific Air or Spark microservices with a stable API surface but a single consumer team that co-deploys with the provider | Medium | Low risk of silent drift when teams co-deploy, but useful once the API stabilises to prevent regressions during future ownership changes or team growth. |
| TransitNZ or HealthNZ internal tooling where the API is in rapid daily flux and the schema changes every sprint | Low | Contracts written this week will be stale next week. Wait for the API to stabilise, then introduce contracts to lock the agreed shape before other consumers onboard. |
| Single-repo monolith with one deployment pipeline (common in early-stage NZ SaaS startups) | Low | All services change together and integration tests cover the boundary already. Contract tests add overhead without protecting independently-deployable services that do not yet exist. |
Trade-offs
What you gain and what you give up when you choose contract testing.
| Advantage | Disadvantage | Use instead when… |
|---|---|---|
| Breaking API changes fail the provider build immediately, before any service ships — not in staging or production. | Both teams must adopt the broker and enforce verification in CI; one side opting out makes contracts useless. | Teams share a repo and co-deploy — a shared integration test suite is simpler and equally effective. |
| Consumer and provider CI run in parallel without needing a shared environment; no staging-environment contention or flaky environment failures. | Contract tests only verify the API boundary (request/response shape) — they do not cover authentication flows, network routing, or data-plane behaviour between live services. | You need full end-to-end coverage of a user journey — use an integration or smoke test suite in a real environment after contracts pass. |
| The broker names exactly which consumer is affected by a provider change, replacing vague "something broke in staging" with precise ownership. | Contracts require maintenance: stale contracts from decommissioned consumers block legitimate provider changes if not audited regularly. | The API is in rapid flux with daily schema changes — hold off until the shape stabilises, otherwise contract maintenance costs outweigh the protection. |
| Consumer contracts lock down exactly which fields are depended on, freeing the provider to add fields, refactor internals, and evolve safely without breaking unknown consumers. | Version discipline is critical: the broker shows green only if verification runs against the exact commit SHA being deployed, not just "whatever last passed on main". | You are integrating with a third-party API that cannot run your pact verifications — write consumer contracts to detect drift locally, but accept that provider-side enforcement is not possible. |
Enterprise reality
At 200–300-developer scale in NZ enterprise, contract testing shifts from a team-level practice into a cross-organisational governance function — with tooling, compliance obligations, and coordination overhead that small teams never encounter.
- Contract generation and broker publication are fully automated in CI — no developer manually runs
pact publish. PactFlow (or a self-hosted Pact Broker) gates every deployment viacan-i-deployhooked into the release pipeline; in small teams this discipline is aspirational, in enterprise it is enforced by the platform team as a mandatory pipeline step. - KiwiFirst Bank and Coastal Bank-scale organisations operating under the Privacy Act 2020 and NZISM security controls require contract artefacts to be retained as evidence of API change governance — the broker history becomes an audit trail showing which consumer team approved a provider field removal and when. PCI DSS environments add a further requirement: contracts covering cardholder-data fields must be reviewed by a security architect before the provider verification is permitted to pass.
- At 10+ squad scale, teams standardise on PactFlow (SaaS) for its webhooks, team-level access controls, and network diagrams that show the full consumer–provider dependency graph. Self-hosted Pact Broker becomes unmanageable when 40+ services are publishing contracts daily and the platform team needs to audit stale contracts, enforce branch tagging, and trigger provider verification across multiple provider repos automatically.
- Cross-team coordination is formalised: provider teams cannot remove or rename a field without raising a contract change request to every affected consumer squad, typically managed via a service-mesh or API governance guild. In a 5-squad org this is a Slack message; in a 30-squad org like TeleNZ’s digital division it requires a structured deprecation period (commonly 2–4 sprints), documented in an RFC, with the old field kept live until every consumer’s contract is updated and verified against the new schema.
◆ What I would do
Professional judgment — when to reach for contract testing, when to skip it, and what to watch for.
The bottom line: Contract testing pays for itself the moment two independently-deployed services are owned by different teams. Introduce it early enough to gate the first joint deployment, not after the first production incident caused by silent API drift.
6 Best Practices
- ✓ Use type matchers, not fixed values, for generated data. Assert that
transactionIdis a non-empty string, not that it equals“TXN-456”. This keeps the contract robust when the provider changes its ID generation strategy. - ✓ Only assert on fields the consumer actually reads in code. If the consumer only displays
referenceNumberandstatus, don’t assert onprocessingTimeor audit metadata. Narrow contracts are easier to maintain and allow the provider to evolve freely. - ✓ Make provider verification mandatory in CI — not optional. A contract test that can be skipped is a contract test that will be skipped the week before a release. Block merges on contract failures.
- ✓ Name interactions clearly enough that failures are self-explanatory. “a valid GST return submission for a sole trader” tells you far more than “test 1” when a CI check fails at midnight.
- ✓ Publish contracts from every consumer branch, not just main. This surfaces breaking changes before a consumer PR is merged, not after. PactFlow and the open-source Pact Broker both support branch-tagged contracts.
- ✓ Use provider states to set up the data the contract needs. A provider state like “order ORD-123 exists and is pending” makes contracts deterministic; testing against a random DB state means flaky results.
- ✓ Review contracts as part of an API change PR. When a provider PR renames or removes a field, the contract failure comment in CI is the review. Treat it like a test failure — do not merge until the consumer has migrated or the contract is renegotiated.
- ✓ Retire contracts that no longer have an active consumer. Stale contracts block legitimate provider changes. Audit the broker quarterly and delete pacts whose consumers have been decommissioned.
- ✓ Separate contract concerns from business-logic concerns. A contract should verify the API boundary (request shape, response schema, status codes). Complex validation rules and business logic live in unit tests, not contracts.
- ✓ Document breaking-change policy in the team agreement. Decide in advance: will the provider support the old field for one sprint while consumers migrate, or will they negotiate a version bump? Having the policy written down avoids a heated Slack thread at release time.
7 Common Misconceptions
❌ Myth: Contract tests replace integration tests, so we can delete our integration test suite.
Reality: Contract tests verify the API boundary in isolation — they check that the request/response shape is correct, not that two live services work together end-to-end. You still need at least a thin layer of integration or smoke tests in a real environment to catch infrastructure misconfiguration, auth failures, network routing issues, and data-plane problems that only appear when both services are actually running together.
❌ Myth: The provider team writes the contract because they own the API.
Reality: Contracts are consumer-driven: the consumer writes them to document exactly what it needs. If the provider wrote the contract, it would describe what the provider offers, not what the consumer depends on — and the two are often different. This distinction is crucial: a provider can add ten new fields and not break a single consumer contract, because consumers only assert on what they use.
❌ Myth: We already have a shared OpenAPI spec, so we don’t need contract tests.
Reality: An OpenAPI spec describes the provider’s interface; it says nothing about which consumer depends on which field. A provider can change a spec in a backwards-compatible way on paper and still break a specific consumer that relied on an undocumented invariant. Contract tests capture the consumer’s real runtime expectations and verify the live code against them on every build — something a static spec file cannot do.
8 Now You Try
Three graded exercises — spot, fix, then build. Write your answer, run it for AI feedback, then compare to the model answer.
An PostNZ tracking service is the provider. A retailer's order-status page is the consumer, and its contract expects this response from GET /track/{id}: { "status": "in_transit", "eta": "2026-06-10", "carrier": "PostNZ" }. The provider team wants to make three changes: (a) add a new field lastScanLocation, (b) rename eta to estimatedDelivery, (c) change status values from lowercase to uppercase (e.g. IN_TRANSIT). For each change, say whether it breaks the contract and why.
Show model answer
(a) Adding lastScanLocation — does NOT break the contract. The consumer only asserts on the fields it needs (status, eta, carrier). A well-written contract uses partial/loose matching, so extra fields the consumer ignores are fine. This is the safe, backwards-compatible way to evolve an API. (b) Renaming eta to estimatedDelivery — BREAKS the contract. The consumer reads "eta"; after the rename that field is gone, so the consumer gets null/undefined. The provider's contract verification should fail in CI. To do this safely, the provider must add estimatedDelivery while keeping eta until every consumer has migrated, then negotiate removing eta. (c) Changing status to uppercase — BREAKS the contract IF the contract pins the exact value "in_transit". The consumer expects a specific string; "IN_TRANSIT" no longer matches, so the consumer's status logic fails. (If the contract only checked the field exists and is a string, it might pass the contract but still break the consumer's display logic — which is why contracts should reflect what the consumer truly depends on.)
A team's contract testing has three problems described below. For each, explain what is wrong and how to fix it.
1. The consumer's mock provider returns a hard-coded
{ status: "OK" } for any request, regardless of input.2. The provider never runs contract verification in CI — they verify "when they remember to".
3. The contract asserts on the provider's internal database transaction count, not just the API response.
For each: what is wrong, and the fix:
Show model answer
Problem 1 — Over-permissive mock: a mock that returns {status:"OK"} for any input lets the consumer and the real provider diverge silently. The contract will pass even though the real provider returns something different. Fix: the mock must return exactly what the real provider returns for each specific request/response interaction, so the published contract reflects reality. The provider then verifies its real code against that same interaction.
Problem 2 — No CI verification: if the provider only verifies "when they remember", breaking changes slip through — which defeats the entire point. Fix: make provider contract verification a mandatory, automated step in the provider's CI pipeline, pulling all consumer contracts from the broker on every build. A failed verification must block the merge/deploy.
Problem 3 — Asserting on internal behaviour: contracts should specify the API boundary (request and response shape), not internal implementation like database transaction counts. Coupling the contract to internals makes it brittle and stops the provider refactoring freely. Fix: assert only on the observable API contract (status code, headers, body schema, data types); leave internal logic to the provider's own unit tests.
A myIR-style tax portal (consumer) calls an Revenue NZ GST service (provider) at POST /gst/return with { irdNumber, period, salesTotal } and needs back a confirmation with a reference number, the accepted status, and the GST amount calculated. Write the consumer-driven contract for this one interaction: describe the request, the expected response, and which fields the consumer actually depends on (so loose matching can be used for the rest).
Show model answer
Interaction: "a valid GST return submission".
Request:
POST /gst/return
Content-Type: application/json
Body: { "irdNumber": "123-456-789", "period": "2026-Q1", "salesTotal": 11500.00 }
Expected response:
Status: 201
Body: {
"referenceNumber": "GST-2026-0001",
"status": "accepted",
"gstAmount": 1500.00
}
Fields the consumer truly depends on (assert exactly / with type matchers):
- status code 201
- status == "accepted" (drives the success screen)
- referenceNumber present, a non-empty string (shown to the user as proof of filing)
- gstAmount present, a number (displayed back for confirmation)
Fields it can ignore (loose match): any extra metadata the provider adds — timestamps, processing IDs, audit fields. Using type matchers (e.g. "a string", "a number") rather than fixed literals where the exact value is generated keeps the contract robust: it verifies the SHAPE the consumer needs without over-specifying. The provider can then add fields freely without breaking the contract (as in Exercise 1a).
Why teams fail here
- They write contracts from the provider perspective instead of the consumer actual usage
- They run contract tests only in CI, not as a gate before provider deployments
- They never publish contracts to a broker, so providers do not know what consumers depend on
- They treat contract tests as a replacement for integration tests rather than a complement
How this has changed
The field moved. Here is how Contract Testing evolved from its origins to current practice.
Integration testing means deploying the whole stack and running end-to-end tests. When services change their APIs, downstream consumers discover the breakage in staging or production. No systematic mechanism exists to catch API contract violations early.
Pact open-sourced as a consumer-driven contract testing framework. The idea: consumers define what they need from a provider, providers verify they meet those needs, without either side needing to be deployed simultaneously. A fundamentally new approach to integration testing.
Pact Broker enables contract sharing across teams in large organisations. The PactFlow SaaS platform makes the broker available without self-hosting. Consumer-driven contract testing begins appearing in microservices architecture guidance as a first-class practice.
Bidirectional contract testing introduced — providers can publish their OpenAPI spec and consumers can verify against it without writing Pact tests. GraphQL and event-driven (Kafka, SNS) contract testing support matures.
Contract testing is standard practice in mature microservices organisations. AsyncAPI extends contract testing to event-driven architectures. The challenge has shifted from technology to culture — getting both consumer and provider teams to treat contracts as shared responsibilities rather than individual concerns.
Interview Questions
What NZ hiring managers ask about Contract Testing — and what strong answers look like.
Explain consumer-driven contract testing and why it is preferable to integration testing against a shared staging environment.
Strong answer: In consumer-driven contract testing (e.g., Pact), the consumer defines what it needs from the provider in a contract file — the specific endpoints, request formats, and response fields it uses. The provider runs the contract tests against its own code, verifying it can satisfy all consumers without requiring any consumer to be deployed. Compared to shared staging: contracts run in CI on every commit (fast feedback), consumers and providers develop independently without deployment coordination, and the test scope is precise — only the contract fields matter, not the entire API surface. Shared staging tests the full system but requires coordination, flakes on environment issues, and makes it hard to attribute failures.
Mid/Senior
What is the difference between a Pact provider test and a Pact consumer test?
Strong answer: The consumer test generates the contract — the consumer writes interaction examples (given this request, I expect this response structure), runs them against a Pact mock, and the result is a contract file (pact.json). The provider test verifies the contract — the provider runs the pact.json file against its actual running service, checking that it returns the expected response for each consumer interaction. Consumer tests run as part of the consumer's CI. Provider tests run as part of the provider's CI and fetch the latest consumer contracts from the Pact Broker.
Junior/Mid
A provider team says adding contract testing will slow their CI by 20%. How do you make the case for it?
Strong answer: I acknowledge the CI cost and reframe against the actual cost of the alternative. The current approach requires a shared staging environment and synchronised deployments between teams. When staging is broken, both teams are blocked — that downtime is a hidden cost not counted in the 20%. Contract tests eliminate the dependency: each team can deploy independently and verify contracts in their own CI. I would also note that the 20% CI cost is a one-time addition; the staging coordination cost occurs on every deployment cycle. In a microservices architecture, contract tests typically save more time than they cost once teams have more than two or three interacting services.
Senior/Lead
Self-Check
Click each question to reveal the answer.
Q1: What does “consumer-driven” mean in contract testing?
The consumer defines what it needs from the provider — the expected request and response — and that becomes the contract. The provider then verifies its real code can satisfy every consumer's contract. Coverage is driven by what consumers actually use, not by everything the provider could theoretically return.
Q2: Why are plain mocks not enough, and how does a contract improve on them?
A mock lets the consumer and provider drift apart silently — the consumer tests against a stale mock while the real provider changes. A contract makes the agreement explicit and testable on the provider's side: if the provider breaks it, their CI fails immediately, before merge, naming the affected consumer.
Q3: Walk through the four steps of the PACT flow.
(1) The consumer test runs against a PACT mock provider and generates a contract (pact) file. (2) The consumer's CI publishes the pact to a broker. (3) The provider's CI pulls all pacts and verifies its real code against each one. (4) If every contract passes, both services can deploy independently with confidence.
Q4: Why is adding a new response field usually safe, but renaming an existing one is not?
A well-written consumer contract asserts only on the fields it depends on and uses loose matching for the rest, so an extra field it ignores does no harm. Renaming a field the consumer reads removes something it relies on — the consumer gets null — so verification fails. To rename safely, add the new field alongside the old one until all consumers migrate.
Q5: Do contract tests replace integration and end-to-end tests?
No. Contract tests verify the API boundary — the request/response shape — quickly and in parallel, catching breaking changes early. They do not exercise full end-to-end scenarios. You still run integration tests, but less often, after the contract tests pass. Contracts are unit tests for the boundary, not a replacement for the whole pyramid.
Q6: Your team is building an Benefits NZ Benefits portal that calls a separate Eligibility Service. The Eligibility Service is owned by a different squad and deploys on its own schedule. Should you introduce contract testing here, and if so, which side writes the contract?
Yes — this is exactly the scenario contract testing is designed for: two independently deployable services owned by different teams. The Benefits portal (consumer) writes the contract, because it is the consumer that knows which fields it actually reads — say, eligibilityStatus and paymentRate — rather than everything the Eligibility Service could return. Publishing that contract to a broker means the Eligibility Service squad's CI will fail the moment they change a field the portal depends on, before either service ships to production.
Q7: What is the key difference between contract testing and API mocking, and when would you choose one over the other?
A mock is a one-sided stub: it makes the consumer's tests pass against a hand-crafted response, but nothing verifies the real provider ever matched that response. Contract testing closes the loop — the mock is generated from the consumer test and then replayed against the real provider code in CI. Choose mocking alone when you have a single team controlling both sides and can update the mock immediately when the provider changes. Choose contract testing when teams are separate and deploy independently, because the contract is the safety net that alerts the provider team the moment their change would break a consumer.
Q8: A developer on your squad says “We have a full OpenAPI spec shared between the Revenue NZ gateway and our myIR consumer, so we don't need contract tests.” What is wrong with this reasoning, and how do you respond?
An OpenAPI spec describes everything the provider offers; it does not capture which fields each specific consumer actually reads at runtime. The Revenue NZ gateway could change a field in a spec-compliant way — for example making a previously optional field required, or tightening an enum — and the spec would still validate while the myIR consumer breaks. Contract tests record the consumer's real runtime expectations and verify the live provider code against them on every build. You would respond: “The spec tells us the provider's intent; the contract tells us the consumer's dependency. We need both.”
Q9: When should you NOT introduce contract testing, even if the system uses microservices?
Avoid contract testing when the API is still in rapid flux and the schema changes daily — contracts will be out of date before the ink is dry, and maintaining them adds friction without protecting anything stable. Also skip it when the consumer and provider are owned by the same developer or very small team and always change together; a shared integration test is cheaper and equally effective. Finally, do not introduce contract tests if the team has no CI pipeline capable of running them automatically — a contract that is only verified “when someone remembers” gives false confidence and is worse than no contract at all. Fix the pipeline first, then add contracts once enforcement is guaranteed.
The Contract Testing — PACT (Specialised) track goes further: multi-lesson deep-dive with NZ-specific compliance context, advanced tooling, and practice exercises. Recommended once you have the fundamentals on this page.