Contract Testing: How to Test Integrations Between Services
Learn how consumer-driven contract tests check service interactions, how to verify providers with Pact, and what contract tests cannot guarantee.
Contract testing checks whether two services agree on the messages exchanged at their integration boundary. In a consumer-driven workflow, the consumer tests the interactions it needs against a mock and records them as a contract; the provider verifies those interactions against its implementation. This can catch compatibility problems without deploying both services together for every test. It does not prove that the complete deployed system works.
1. Define the contract boundary
A contract is the shared expectation for messages at a particular integration seam, not a legal document or a complete description of either service.
- HTTP: the consumer initiates a request and the provider responds. An interaction records the expected request and the minimum response the consumer needs.
- Asynchronous messages: the consumer reads messages and the provider or producer writes them. A message interaction records the minimum message the consumer needs.
Consumer-driven contracts focus on actual consumer usage. They need not describe every state or capability a broad resource schema permits. That focus makes them useful for checking whether a provider change still supports the consumers whose interactions are represented.
2. How a Pact workflow fits together
Pact is one documented code-first approach for testing HTTP and message integrations with contract tests. The following sequence describes its consumer-driven model; other contract-testing approaches may organize the steps differently.
- Write a consumer test against a Pact mock. Exercise the consumer code with the request or message it needs and assert the behavior that depends on the response or message.
- Record the interaction. Pact writes the expected interaction to a JSON contract (a pact). Keep expectations focused on fields and behavior the consumer actually relies on.
- Share the contract. Publish or otherwise make the pact available to the provider verification job. A Pact Broker is one option in Pact workflows; teams can also share contracts through their chosen pipeline.
- Run the provider for verification. Start the provider locally or in the verification environment, retrieve the relevant pact, and replay its requests against the provider. For message contracts, verify that the provider can produce the expected message.
- Set up each interaction’s preconditions. Use provider states to establish the data or conditions needed for that interaction. Keep interactions independently verifiable rather than depending on an earlier interaction to create shared state.
- Put both sides in CI. Run the consumer test when consumer code changes and provider verification when provider code changes or a new contract is available. Make the result visible to the teams deciding whether a change is compatible.
Provider verification commonly stubs the provider’s external dependencies. This keeps the test focused on the contract boundary and can make it more deterministic; retain separate tests for behavior that depends on those real systems.
3. Runnable illustrative example
This compact JavaScript example illustrates the shape of a consumer-side Pact HTTP test. It assumes a Node project with Pact JS and a test runner installed and configured. Pin compatible package versions in your project lockfile and consult the current Pact JS consumer guide for the API supported by your installed version. The example assumes consumer code exports getAccount from ./account-client.js and calls the mock server URL passed to it.
import { describe, it, expect } from 'your-test-runner';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { getAccount } from './account-client.js';
const { like } = MatchersV3;
describe('account consumer contract', () => {
const pact = new PactV3({
consumer: 'web-client',
provider: 'account-service',
});
it('gets an account by id', async () => {
pact
.given('an account exists')
.uponReceiving('a request for an account')
.withRequest({
method: 'GET',
path: '/accounts/42',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: { id: like('42'), status: like('active') },
});
await pact.executeTest(async (mockServer) => {
const account = await getAccount(mockServer.url, '42');
expect(account.id).toBe('42');
expect(account.status).toBe('active');
});
});
});
The provider-side verification needs a running provider and a way to establish the named state. The following is deliberately pseudocode because provider startup, state setup, and verifier APIs depend on the application and the Pact language implementation:
start provider on a local test port
configure provider state handler:
when state is "an account exists": arrange account 42
load pact for consumer "web-client" and provider "account-service"
replay each recorded interaction against the provider
fail verification if status, required headers, or required body fields differ
stop provider and clean up arranged state
For a Go implementation, follow the official Pact Go provider verification guide for its concrete verifier setup. Its documented flow similarly retrieves contracts and replays requests against a locally running provider.
4. Model provider state explicitly
A provider state is a setup condition for one interaction, such as the hypothetical condition “an account exists.” The state handler arranges the data or conditions needed before that interaction is replayed.
- Give states clear names that describe preconditions, not test order.
- Make state setup repeatable and safe to run for each interaction.
- Do not rely on one interaction having run first to create records needed by another.
- Clean up or isolate data so concurrent verification runs do not collide.
- Stub downstream services where their behavior is outside the contract being verified.
Provider states make the precondition visible. They do not assert that a production database or downstream dependency is configured correctly; test those concerns at the appropriate integration or deployment layer.
5. What contract tests cover—and what they do not
| Test layer | Question it can answer | What remains outside that check |
|---|---|---|
| Consumer contract test | Does consumer code make the recorded interaction and handle the expected response or message? | Whether the real provider or deployed environment behaves that way. |
| Provider contract verification | Does this provider implementation satisfy the selected recorded interactions under their configured states? | Unrecorded consumer needs, full workflows, and production infrastructure. |
| Schema or specification conformance | Does provider behavior conform to the provider-authored API or message specification? | Whether each consumer’s real usage expectations are represented and met. |
| End-to-end or deployment checks | Do services, infrastructure, configuration, and dependencies work together in the target environment? | They may be slower or more operationally involved; they address a broader question than the contract seam. |
A passing contract test means the selected expectations passed. It does not prove every possible interaction, the entire application behavior, or that production deployment works. Keep functional, broader integration, and deployment-level checks for behavior not expressed in the contract.
6. Consumer contracts and schemas answer different questions
| Axis | Consumer-driven interaction contract | Provider schema or specification check |
|---|---|---|
| Source of expectations | Interactions exercised by a consumer and recorded in its test. | A provider-authored API or message description. |
| What is checked | Concrete request/response or message interactions selected for that consumer. | Conformance to the declared set of shapes, fields, and rules. |
| Confidence provided | The tested consumer interactions match provider behavior under verification setup. | The provider implementation matches its published description to the extent checked. |
| Possible gap | Only represented interactions are checked; the contract may omit other valid capabilities. | A valid schema can still describe behavior a particular consumer does not use or misunderstand. |
These methods can complement each other. A schema check can help keep implementation and API documentation aligned; consumer interactions add assurance about specific consumer expectations. Which checks are useful depends on the risks and responsibilities of the services involved.
7. CI, performance, and reliability practices
- Keep interactions minimal. Assert the fields, headers, and statuses the consumer needs. Overly strict expectations can create noise when irrelevant provider details change.
- Use deterministic provider states. Arrange only the necessary preconditions and stub dependencies outside the seam under test.
- Run verification close to changes. Consumer tests should produce updated contracts; provider CI should verify relevant contracts before a change is treated as compatible.
- Track which contract version was checked. In teams using a broker or shared contract store, associate verification results with the relevant application revision and deployment workflow.
- Retain broader checks. Do not remove end-to-end or environment checks solely because contract verification passes.
- Diagnose failures at the boundary. Separate a mismatch in the interaction from a provider startup failure, state setup issue, or unrelated dependency error.
Contract verification often avoids bringing up the full set of services, which can make it simpler than an end-to-end environment. Actual runtime and reliability depend on the test suite, provider startup, state handlers, and dependencies; no fixed speed or defect-reduction claim follows from the method itself.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Consumer test reports an unexpected request. | The consumer sends a different method, path, header, query, or body than the interaction describes. | Inspect the actual request and update either the consumer behavior or the interaction to reflect the intended usage. |
| Provider returns a mismatch during verification. | Status, required header, or expected body fields differ from the recorded interaction. | Check the failing interaction and decide whether the provider broke a real consumer need or the contract encodes an unnecessary expectation. |
| Provider state setup fails. | The state name is missing, setup assumes hidden ordering, or its backing dependency is unavailable. | Implement the named state handler, make it independently repeatable, and stub dependencies outside the contract boundary where appropriate. |
| Verification passes locally but fails in CI. | Environment configuration, provider startup timing, port binding, or test data differs. | Make startup and state setup explicit, avoid shared mutable data, and inspect CI logs for the first failing interaction or setup step. |
| Provider verification cannot retrieve a contract. | Publication, broker credentials, selectors, or contract version selection are incorrect. | Confirm the consumer published the expected pact and that the verifier is configured to fetch that consumer/provider pair and revision. |
| Unrelated provider changes keep breaking contracts. | Interactions assert incidental details or consumers have stale contracts. | Remove expectations consumers do not depend on, and ensure CI verifies the currently relevant consumer contracts. |
| All contract checks pass but the deployed flow fails. | The issue is in unrepresented behavior, configuration, infrastructure, or a real downstream integration. | Add or repair the appropriate functional, end-to-end, deployment, or integration test; a passing contract only covers its selected seam expectations. |
9. A practical adoption checklist
- Choose one consequential service boundary and identify its actual consumer and provider.
- Record a small number of representative consumer interactions, including an important failure or optional-field case where relevant.
- Ensure provider states set up each interaction independently.
- Run provider verification against the provider implementation, with external dependencies controlled.
- Publish or share contracts and verification results in a way both service teams can see.
- Keep schema, functional, broader integration, and deployment checks for their separate assurance goals.
- Review failed interactions with both teams before changing a contract merely to make CI green.
10. FAQ
Does a contract test require both services to run?
The consumer test commonly uses a Pact mock, while provider verification runs the provider implementation. You do not need to deploy both services together for every contract check.
Is a contract the same thing as an OpenAPI document?
No. A consumer-driven contract records selected consumer interactions. An API specification describes the provider’s API more broadly. Both can be useful for different checks.
Can contract testing cover asynchronous systems?
Yes. The contract can describe messages exchanged through a queue or similar boundary. In Pact’s terminology, the consumer reads the message and the provider or producer writes it.
Does a passing pact prove production is safe?
No. It proves only that the selected interactions passed under the verification setup. Production behavior, configuration, infrastructure, and unrecorded flows need other checks.
Or skip the browser setup
Contract tests check service messages; when your workflow also needs a website screenshot for documentation, review, or an AI agent, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. The example below requests a WebP screenshot; see the ScreenshotNeo API documentation for options.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners, newsletter popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, take screenshots.
- 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.


