Contract Testing
What it is
Section titled “What it is”A contract test verifies that a consumer and a provider agree on their interface — the request/response shapes, status codes and events between two services — without deploying both together.
The consumer records its expectations as a contract (a pact). The provider runs that contract against its real implementation. If both pass, the integration is safe to ship.
Consumer test ──► pact file ──► Provider verification (expectations) (JSON) (runs real provider)This replaces fragile end-to-end environments with fast, independent checks — a natural fit for microservices and mobile/backend boundaries.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Microservices and any consumer/provider split across teams
- Mobile app against a backend API
- Any interface where “we think we agree” is the risk
Avoid it when
- There is one deployable, so a normal integration test is simpler
- The API is trivial and stable and a schema test suffices
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| Pact | Multi (JS, Java, Go, .NET, Python, Ruby) | The de-facto standard; broker for sharing |
| PactFlow | Multi | Hosted Pact broker + can-i-deploy |
| Spring Cloud Contract | Java | Producer and consumer verification |
| Schemathesis | Multi | Property-based testing from OpenAPI schemas |
| Dredd | Multi | Validate implementations against OpenAPI/API Blueprint |
| Postman / Newman | Multi | Contract assertions in collections |
Quick setup
Section titled “Quick setup”npm install -D @pact-foundation/pact<dependency> <groupId>au.com.dius.pact.consumer</groupId> <artifactId>junit5</artifactId> <version>4.6.15</version> <scope>test</scope></dependency>go get github.com/pact-foundation/pact-go/v2pip install pact-pythondotnet add package PactNetExample
Section titled “Example”import { PactV3, MatchersV3 } from '@pact-foundation/pact';const { like, integer } = MatchersV3;
const provider = new PactV3({ consumer: 'web', provider: 'users-api' });
it('gets a user by id', async () => { provider .given('user 42 exists') .uponReceiving('a request for user 42') .withRequest({ method: 'GET', path: '/users/42' }) .willRespondWith({ status: 200, body: { id: integer(42), name: like('Ada') }, });
await provider.executeTest(async (mock) => { const user = await getJson(`${mock.url}/users/42`); expect(user.name).toBe('Ada'); });});@ExtendWith(PactConsumerTestExt.class)class UserConsumerTest { @Pact(consumer = "web", provider = "users-api") public RequestResponsePact user42(PactDslWithProvider builder) { return builder .given("user 42 exists") .uponReceiving("a request for user 42") .path("/users/42") .method("GET") .willRespondWith() .status(200) .body("{\"id\":42,\"name\":\"Ada\"}") .toPact(); }}pact. AddInteraction(). Given("user 42 exists"). UponReceiving("a request for user 42"). WithRequest(dsl.Request{Method: "GET", Path: "/users/42"}). WillRespondWith(dsl.Response{Status: 200})Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- One contract per consumer. Each consumer owns its own expectations.
- Use matchers, not exact values.
like('Ada')is not the point; the shape is. - Version the contract and share it via a broker. Then ask “can I deploy?” before release.
- Do not contract-test internal functions — only the network boundary counts.
- Gate deploys with
can-i-deployso a provider is only released if all consumer contracts still pass.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.