Contract Testing
When two services talk over HTTP, each side has its own tests — but nothing guarantees they agree on the contract: the exact paths, status codes, and JSON shapes exchanged. A provider can rename a field, pass all its own tests, and silently break every consumer in production. Contract testing closes that gap by turning the agreement itself into an executable, shared artifact.
The dominant style is consumer-driven contracts (CDC): consumers declare what they need, providers prove they still deliver it. This page covers Spring Cloud Contract, the native fit for Spring Boot, and contrasts it with Pact.
The problem it solves
Without contracts you have two bad options. End-to-end tests that spin up both services are slow, flaky, and hard to run in CI. Independent mocks drift — the consumer’s stub of the provider gradually diverges from reality. Contract testing gives each side a fast, isolated test while keeping a single source of truth in sync:
| Approach | Speed | Catches drift | Needs both services running |
|---|---|---|---|
| Full end-to-end | Slow | Yes | Yes |
| Hand-written mocks | Fast | No | No |
| Contract testing | Fast | Yes | No |
Spring Cloud Contract — the producer side
The producer (the API provider) defines contracts describing each interaction. Add the verifier and a base test class:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.example.rates.ContractBase</baseClassForTests>
</configuration>
</plugin>
Write a contract in the Groovy DSL under src/test/resources/contracts/:
// shouldReturnUsdToEurRate.groovy
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "should return the USD to EUR rate"
request {
method GET()
url "/v1/usd/eur"
}
response {
status OK()
headers { contentType applicationJson() }
body([ value: 0.92 ])
}
}
At build time the plugin does two things:
- Generates a provider test that boots your controller and asserts it actually fulfils the contract.
- Packages a stub JAR (a WireMock mapping) and installs it to the local/remote Maven repo for consumers to download.
The generated test extends your base class, which sets up the context (here using RestAssuredMockMvc):
public abstract class ContractBase {
@BeforeEach
void setup() {
RestAssuredMockMvc.standaloneSetup(new RateController(stubService()));
}
}
If a developer later changes the response shape, the generated provider test fails on mvn test — the contract is broken at the source.
Spring Cloud Contract — the consumer side
The consumer never hand-writes a stub. It pulls the provider’s published stub JAR and runs against it with @AutoConfigureStubRunner:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@AutoConfigureStubRunner(
ids = "com.example:rates-service:+:stubs:8090",
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
class RateClientContractTest {
@Autowired RateClient rateClient;
@Test
void usesProviderStub() {
// Calls localhost:8090, served by the provider's stub
assertThat(rateClient.usdToEur()).isEqualByComparingTo("0.92");
}
}
Because the stub is generated from the same contract the provider is verified against, the consumer test can never drift from the provider. The + in the ids means “latest version”; pin a version in CI for reproducibility.
Note:
stubsMode = LOCALreads stubs from the local Maven repo (great for a monorepo or local dev). UseREMOTEwith arepositoryRootto pull published stubs from Artifactory/Nexus, or a Git-based stub store, when services live in separate pipelines.
Spring Cloud Contract vs Pact
Both implement consumer-driven contracts but invert who writes them:
| Spring Cloud Contract | Pact | |
|---|---|---|
| Contract author | Producer-centric (provider owns the DSL) | Consumer-centric (consumer generates from its test) |
| Contract format | Groovy / YAML / Java DSL | JSON pact file |
| Generated stubs | WireMock mappings | Pact mock server |
| Sharing | Maven repo, Git, or artifact store | Pact Broker (versioning + verification matrix) |
| Ecosystem | JVM-first, deep Spring integration | Polyglot (JS, Go, .NET, Ruby, JVM…) |
Tip: Choose Spring Cloud Contract for an all-JVM estate already on Spring Cloud — the WireMock stubs double as a local dev sandbox. Choose Pact when consumers and providers span multiple languages and you want the Pact Broker’s can-i-deploy verification matrix.
Where it fits in microservices
Contract testing sits between unit tests and full integration in your test pyramid. In a microservices system every pair that performs inter-service communication should share a contract:
- The provider’s CI fails fast if a change would break a consumer.
- The consumer’s CI runs against an always-accurate stub, with no live provider needed.
- You avoid brittle, slow end-to-end suites for the bulk of API-compatibility checks.
Warning: Contracts verify structure and protocol, not business correctness. A provider can satisfy the contract while returning wrong data — keep your normal unit and integration tests for behaviour, and use contracts purely to guard the wire format between services.