How to Use Contract Testing in a Microservices Architecture
Learn how consumer and provider contract tests catch breaking service changes, automate Pact verification in CI, and use compatibility results to guide deployments.
Contract testing checks whether two independently developed services agree on the requests and responses—or messages—they exchange. In a consumer-driven Pact workflow, the consumer tests its real client code against a mock provider; Pact generates a contract from those interactions; then the provider verifies that its implementation satisfies the contract. Run both sides in CI and use recorded verification results to inform deployment decisions.
This gives teams fast, boundary-level compatibility evidence without deploying the whole system for every check. It does not prove that an entire distributed workflow works, nor does it replace provider functional tests or the end-to-end tests needed for system behavior.
1. Understand consumer and provider roles
Roles describe the direction of an interaction, not a team’s organizational position. For HTTP, the consumer initiates a request and the provider returns a response. For asynchronous messaging, the consumer reads a message and the provider produces it. This terminology still works when there is no conventional client-server exchange.
A Pact contract is a set of concrete interactions that matter to consumers. Each interaction describes a request or message and the expected response or message. Pact’s [consumer documentation](https://docs.pact.io/consumer) recommends testing the actual API client and focusing on behavior the consumer relies on. A test that bypasses the real client can produce a contract without checking whether application code constructs or handles the interaction correctly.
2. Choose the contract testing approach
| Approach | What it checks | Useful when | Limit |
|---|---|---|---|
| Consumer-driven interaction contracts | Executable examples of interactions current consumers use | Teams need to evolve services independently while checking actual consumer assumptions | Does not enumerate every possible valid API state |
| Provider conformance to a schema | Whether provider behavior aligns with a published specification such as OpenAPI | Teams need broad documentation-to-implementation alignment | By itself, does not establish that consumers call the API correctly or that their particular expectations are met |
These approaches can complement each other. Use the one whose evidence matches the question: “Does this implementation honor the documented API?” differs from “Does this provider satisfy the interactions this consumer uses?”
3. Map boundaries and consumer needs
- List service-to-service HTTP calls and asynchronous message flows.
- For each boundary, identify the consumer and provider by interaction role.
- Prioritize interfaces where independent changes could break another service or team.
- For each important interaction, write down the request or message the consumer sends and the minimum response or message fields it actually uses.
- Include meaningful scenarios such as success, not-found, or authorization responses when consumers handle them differently.
Keep contracts focused on used behavior. Asserting fields or exact details the consumer does not rely on makes provider verification brittle and can block harmless changes. A contract is an example of an interaction, not a complete schema of all provider states.
4. Implement a consumer contract test with Pact JS
The example below uses the Pact JS HTTP API to test a small consumer client against Pact’s mock provider. It is illustrative runnable project code; use the Pact package version and test-runner configuration already supported by your project. The consumer test must call the real client function, not make a separate test-only HTTP request.
npm init -y
npm install --save-dev @pact-foundation/pact jest
npm install node-fetch
In package.json, set the project to use ES modules and add a test command. For example:
{
"type": "module",
"scripts": { "test": "jest --runInBand" }
}
Create src/user-client.js:
export async function getUser(baseUrl, id) {
const response = await fetch(`${baseUrl}/users/${id}`, {
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error(`User request failed: ${response.status}`);
return response.json();
}
Create test/user-client.pact.test.js. Pact’s API and module exports can vary by major release; align imports and configuration with the [Pact JS documentation](https://docs.pact.io/implementation_guides/javascript/docs/consumer) for the version pinned in your lockfile.
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { getUser } from '../src/user-client.js';
const { like } = MatchersV3;
const provider = new PactV3({
consumer: 'web-app',
provider: 'user-service',
dir: './pacts'
});
describe('getUser contract', () => {
it('requests a user and reads the fields the app needs', async () => {
await provider
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123',
headers: { Accept: 'application/json' }
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: like({ id: '123', name: 'Ada' })
})
.executeTest(async (mockServer) => {
const user = await getUser(mockServer.url, '123');
expect(user.id).toBe('123');
expect(user.name).toEqual(expect.any(String));
});
});
});
Run the consumer test:
npm test
Pact writes the pact file from the test interaction. The exact artifact location and matcher APIs depend on the Pact JS version and configuration. Commit or publish the generated contract through your team’s chosen workflow; do not maintain a separately hand-edited contract as a substitute for exercising the consumer test.
What makes this a useful consumer test?
- It invokes
getUser, the consumer’s real API client function. - It describes a specific request, including the path and relevant header.
- It uses a type matcher for the name, allowing values beyond the example “Ada.”
- It asserts the fields the consumer uses, not incidental response details.
- The provider state names the precondition required to produce the response.
For a missing-user interaction, add a separate scenario with the expected status and the error behavior the consumer handles. Do not turn every provider implementation branch into a contract interaction unless a consumer depends on it.
5. Verify the provider implementation
The consumer test proves that consumer code works against the expected mock interaction. It does not prove the provider behaves that way. Provider verification replays the recorded interaction against provider code, with the provider placed in the required state. Stub external dependencies where practical so verification stays controlled and repeatable. See Pact’s [provider verification guide](https://docs.pact.io/getting_started/provider_verification) and [Pact JS verifier options](https://docs.pact.io/implementation_guides/javascript/docs/provider).
A simplified provider-side example using Pact JS:
import { Verifier } from '@pact-foundation/pact';
const verifier = new Verifier({
provider: 'user-service',
providerBaseUrl: 'http://127.0.0.1:8080',
pactUrls: ['./pacts/web-app-user-service.json'],
stateHandlers: {
'user 123 exists': async () => {
// Arrange deterministic provider data for this verification.
await testDatabase.users.replace({ id: '123', name: 'Ada' });
}
}
});
await verifier.verifyProvider();
Start the provider locally or in the CI job before running the verifier, and clean up test state afterward. The example’s testDatabase is intentionally an application-specific fixture; connect it to your test database or provider state mechanism. If the provider cannot safely expose state setup, use an isolated test environment or a suitable data-layer fixture. Avoid depending on a public live API to arrange state: that can make verification slower and more brittle.
Provider verification configuration to decide
- Provider URL: point to the local or CI instance of the provider under test.
- Contract source: use a generated local pact file for a simple example, or retrieve relevant consumer contracts from a Pact Broker.
- Provider states: implement deterministic setup for each named precondition used by the contracts.
- External dependencies: stub or control downstream services so a contract failure identifies the provider boundary under test.
- Version metadata: provide provider version and branch/build information when publishing verification results to a Broker.
- Pending contracts and selectors: configure them deliberately for your branch and release policy; do not assume one setting fits every team.
6. Add contract tests to CI/CD
Each service can keep its own build and deployment pipeline. A practical minimum is to run consumer tests when consumer code changes, publish the generated pact so the provider can retrieve it, run provider verification when provider code changes or a contract changes, and make the result available before deployment. A Pact Broker coordinates contract and verification sharing; the CI/CD guide explains that the exact process depends on existing team and release practices ([Pact CI/CD guidance](https://docs.pact.io/pact_nirvana/step_by_step)).
- Consumer CI: run unit and Pact tests; publish the generated pact using a stable application version such as the source revision.
- Provider CI: start the provider, retrieve relevant pacts, apply provider states, run verification, and publish verification results.
- Compatibility gate: before deployment, ask the Broker whether the candidate version has successful verification against the versions already deployed in the target environment.
- Record deployment: after a successful deployment, record which application version entered the environment so future compatibility checks use accurate deployment data.
The Pact Broker’s [can-i-deploy documentation](https://docs.pact.io/pact_broker/can_i_deploy) describes the compatibility matrix and the pre-deployment check. A representative command is:
pact-broker can-i-deploy \
--pacticipant user-service \
--version "$GIT_SHA" \
--to-environment production \
--broker-base-url "$PACT_BROKER_BASE_URL"
After deployment, record the deployed version using the corresponding Broker command:
pact-broker record-deployment \
--pacticipant user-service \
--version "$GIT_SHA" \
--environment production \
--broker-base-url "$PACT_BROKER_BASE_URL"
Keep Broker credentials in your CI secret store. If verification results arrive asynchronously, configure the deployment check to wait or poll as appropriate. Coordinate contract-change-triggered verification so one team’s contract update does not unpredictably disrupt another team’s ordinary build. Pact’s [Broker overview](https://docs.pact.io/pact_broker/overview) explains the role of shared contracts, verification results, and deployment compatibility.
7. Cover asynchronous messaging
The consumer/provider role names apply to messages too: the provider produces a message and the consumer reads it. For a message interaction, define the message shape and the fields the consumer needs; generate the contract from a consumer test; then verify provider message generation against it. Pact supports message testing through language-specific implementations and, in some cases, plugins. Check the [Pact documentation](https://docs.pact.io/) for the specific language and message transport setup rather than assuming the HTTP example transfers unchanged.
Message contracts should represent the payload the consumer actually receives, including relevant metadata where consumer behavior depends on it. They do not prove queue delivery guarantees, ordering, retries, or the correctness of a multi-service business process; test those properties at the appropriate layer.
8. Options, edge cases, and boundaries
| Concern | Practice |
|---|---|
| Extra provider fields | Do not require exact bodies if the consumer ignores additional fields. Match the fields and types the client uses. |
| Authentication | Include only the relevant request behavior; supply safe test credentials or controlled authorization setup in provider verification. |
| Errors and empty results | Add interactions when the consumer responds differently to these outcomes. |
| Changing provider state | Use isolated, repeatable fixtures; do not let scenarios depend on execution order or shared mutable data. |
| Multiple consumers | Verify provider changes against the relevant consumer contracts, not just one team’s pact. |
| Breaking changes | Use compatibility evidence for deployed versions before release; coordinate intentional incompatible migrations across consumers and providers. |
| Schema completeness | Pair interaction contracts with schema or conformance checks if broad API documentation alignment is a requirement. |
| End-to-end behavior | Keep targeted end-to-end tests for cross-service business flows and operational behavior that a boundary contract cannot establish. |
9. Performance, reliability, and cost
Contract tests usually exercise a focused boundary with a mock on the consumer side and a locally running provider during verification. This avoids requiring the full microservices environment for each boundary check. Provider verification is more reliable when state setup and downstream dependencies are controlled. Pact documentation recommends verifying against a local provider and stubbing external dependencies for fast, deterministic checks ([provider verification](https://docs.pact.io/provider)).
Keep the suite efficient by covering interactions consumers use rather than every theoretical combination, using a repeatable test database or fixtures, and running only the relevant verification work on a change while retaining an appropriate full compatibility check before deployment. The research sources provide no benchmark or universal cost figure; CI duration and Broker operating costs depend on the implementation and infrastructure you choose.
Reliability depends on meaningful contracts, stable provider states, current deployment records, and verification evidence associated with the versions actually being considered. A green consumer mock test alone is incomplete evidence, and a stale or missing verification result should not be mistaken for a successful compatibility check.
10. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Consumer Pact test fails to match the mock | The actual client sends a different method, path, query, header, or body than the interaction specifies. | Inspect the real client request and update either the client or the expected interaction to reflect the intended behavior. |
| Consumer test passes but provider verification fails | The provider response differs from the consumer’s recorded expectation, or provider state was not arranged. | Check the failed interaction details, confirm state handler names match, and make test data deterministic. |
| Provider returns 404 or wrong content type | Route, base path, or response serialization differs from what the consumer calls. | Correct the provider route or consumer request based on the intended interface, then rerun verification. |
| Provider verification cannot connect | The server is not running, listens on another port, or startup has not completed. | Start the provider in the verification job, wait for readiness, and confirm the configured base URL. |
| Contract is missing from provider CI | The consumer did not publish it, the provider selects the wrong contract source, or Broker authentication failed. | Check the consumer publish step, provider selectors or pact URLs, network access, and secret configuration. |
| Verification flakes across runs | Provider state relies on shared mutable data, external systems, or timing. | Isolate data, stub dependencies, make setup idempotent, and avoid relying on scenario order. |
| Deployment gate says no or has no result | There is a failed verification, missing version/deployment metadata, or verification has not completed. | Inspect the Broker matrix, publish the missing evidence, correct environment records, and wait for asynchronous verification if configured. |
| Harmless provider changes break contracts | The consumer contract asserts incidental details or exact values it does not use. | Narrow expectations to consumer needs and use appropriate type or format matchers. |
11. Or skip the browser setup
Contract testing verifies service interactions. If your engineering workflow also needs website screenshots—for example, to capture a rendered page as a review artifact—ScreenshotNeo provides a one-request screenshot API. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. See ScreenshotNeo for product details, then sign up for 1,000 free screenshots a month with no card.
12. FAQ
Does contract testing replace integration testing?
No. It checks defined service-boundary interactions. Keep other tests for business workflows, operational behavior, and broader system properties.
Can a consumer mock test prove the provider is compatible?
No. Provider verification must replay the generated contract against provider code.
Should every API response field go into a Pact?
No. Include the fields and behavior consumers depend on. Use schema or conformance testing when you need broader specification coverage.
Can teams deploy independently?
Contract verification and Broker compatibility checks provide evidence to coordinate independent deployments. The teams still need accurate version and environment records and a release process that acts on the results.
Is Pact limited to HTTP?
No. Consumer and provider roles also apply to asynchronous messaging, though implementation details depend on the language and transport.
Primary references: Pact consumer testing, Pact JS provider verification, verifying pacts, can I deploy, and Pact Broker overview.


