Contract Testing: API Reliability Across Services
Contract testing verifies that the producer of an API and its consumers agree on the shape of data and behavior. It is the glue that keeps microservices from breaking each other. Unlike end-to-end tes
Contract testing verifies that the producer of an API and its consumers agree on the shape of data and behavior. It is the glue that keeps microservices from breaking each other. Unlike end-to-end tests, contract tests are fast, reliable, and run without a full environment. This guide covers consumer-driven contracts specifically.
The problem contract tests solve
Service A calls Service B. A is a client; B is a server. Both evolve. A breaking change in B (removed field, renamed endpoint, changed semantics) breaks A.
Options:
- Manual coordination. B team emails A team. Slow, error-prone.
- E2E tests. A and B deployed together, test exercises full flow. Slow, expensive.
- Contract tests. A declares expectations; B verifies. Runs in isolation.
Contract tests catch the A-B breakage at B's PR time, not at runtime.
Consumer-driven contracts (CDC)
The consumer drives what the contract must support. Why? Consumers know what they need. Producers might add fields; consumers only need specific ones.
Flow:
- Consumer declares expected requests and responses (the contract)
- Contract published to shared registry
- Producer's CI verifies it satisfies the contract
- Any producer change that breaks the contract → red build
Tools
Pact
The canonical. Libraries for most languages. Broker for contract registry. Mature.
Spring Cloud Contract
Java / Spring focused. Similar concept.
WireMock
HTTP mocking with record/playback. Not strictly CDC but adjacent.
Mountebank
Multi-protocol stubbing.
Hand-rolled
OpenAPI spec + schema validation at runtime. Simple but limited.
Pact example
Consumer side
@Pact(consumer = "ClientA", provider = "ServiceB")
public RequestResponsePact pact(PactDslWithProvider builder) {
return builder
.given("user with id 42 exists")
.uponReceiving("get user 42")
.path("/users/42")
.method("GET")
.willRespondWith()
.status(200)
.body(newJsonBody(o -> o
.stringType("id", "42")
.stringType("name", "Alice")
).build())
.toPact();
}
During consumer testing, Pact spins up a stub server matching this contract. Tests run against it. Contract is published to broker on pass.
Producer side
@State("user with id 42 exists")
public void userExists() {
// Set up state in producer's DB
userRepository.save(new User("42", "Alice"));
}
During producer CI, Pact fetches contracts from broker. For each contract, sets up state, replays requests, verifies responses match. Fails build on mismatch.
What contracts cover
- Request path, method, query parameters
- Request body shape (fields, types)
- Response status
- Response body shape and types
- Headers
What they do not cover:
- Semantic correctness (is the returned data *right*?)
- Performance
- Non-HTTP (but Pact v4 supports messaging)
Integration with CI
- Consumer CI: run contract tests. Publish on pass.
- Producer CI: pull contracts, verify. Block deploy on fail.
- Dashboard: shows compatibility between versions.
- Can-I-deploy: automated check that producer version N is compatible with all deployed consumer versions.
Anti-patterns
1. Producer-driven contracts
Producer says "here's what I return." Consumer adapts. Gives producer freedom to ship anything, consumers break.
2. Ignoring state
@State setup skipped or incorrect. Contracts pass for wrong reasons.
3. Broker outdated
Contracts in broker are stale. Producer verifies against non-existent consumers.
4. Coupling tests
Consumer's unit tests do double-duty as contract tests. Mixing concerns.
5. No can-i-deploy gate
Contracts exist but deploy still allowed with incompatible versions.
When contracts are not enough
- Performance. Contracts are shape only.
- Race conditions. Order of requests across services.
- End-to-end flows. Multi-service user journeys.
Contract tests + integration + E2E = layered coverage.
How SUSA handles API contracts
SUSA captures all APIs observed during exploration. From this:
- Infers API surface map (endpoints, request/response patterns)
- Detects schema drift between releases (new field, removed field, type change)
- Provides baseline for consumer-side contract authoring
susatest-agent test myapp.apk --persona curious --steps 200
# results/api_surface.json has inferred endpoints
# results/evolution.json has deltas from previous session
This is post-hoc contract discovery. Useful for legacy systems without explicit contracts.
Starting with Pact
- Pick one consumer-producer pair. High-traffic, business-critical.
- Add Pact to consumer tests. Start with one endpoint.
- Set up broker (Pact Broker self-hosted or PactFlow SaaS).
- Producer CI verifies.
- Expand coverage: more endpoints, more consumers.
Contract testing matures teams. Microservices without contracts are fragile; with contracts, durable.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free