Spring Cloud Contract: Consumer-Driven Contract Testing Guide
Learn consumer-driven contract testing with Spring Cloud Contract.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Maven or Gradle
- ✓Basic understanding of microservices and REST APIs
- Use Spring Cloud Contract to define contracts between services as Groovy or Java DSL.
- Contracts are verified by the producer; stubs are generated for consumers.
- Key commands:
mvn clean installruns verifier;mvn spring-cloud-contract:convertgenerates tests. - Always version your contracts and stubs.
- Avoid over-specifying; keep contracts minimal for flexibility.
Think of contract testing like a handshake agreement between two teams. Team A (consumer) says "I need you to give me X when I send Y." Team B (producer) agrees and writes a test to ensure they always deliver X. If B changes their behavior, the contract test breaks immediately, saving A from a production surprise.
In a microservices ecosystem, integration testing is the bane of every developer's existence. You've been there: a service changes its API ever so slightly, and suddenly your downstream consumer starts throwing 500s in production. The classic approach? Deploy both services, run end-to-end tests, and pray. That's slow, brittle, and doesn't scale.
Enter consumer-driven contract testing (CDCT). The idea is simple: the consumer defines what it expects from the producer, and the producer verifies it can meet those expectations. Spring Cloud Contract is the de facto tool for this in the Java ecosystem. It lets you define contracts as Groovy or Java DSL, auto-generates tests for the producer, and produces stubs that consumers can use in their own tests.
I've seen teams cut their integration testing time by 80% after adopting Spring Cloud Contract. But I've also seen teams misuse it — creating massive, brittle contracts that defeat the purpose. This guide will show you how to do it right, with real production lessons from the field.
What is Consumer-Driven Contract Testing?
Consumer-driven contract testing (CDCT) flips the traditional testing pyramid. Instead of writing integration tests that require all services to be up, each consumer defines its expectations in a contract. The producer then verifies it can fulfill those contracts. If a change breaks a contract, the producer's build fails immediately.
Spring Cloud Contract implements CDCT by allowing you to define contracts in Groovy or Java DSL. These contracts describe HTTP requests and responses (or messaging). From these contracts, the framework generates: - Producer-side tests: JUnit tests that verify the producer's API matches the contract. - Stubs: WireMock-compatible stubs that consumers can use in their tests.
Here's the hard truth: most teams get this wrong. They either write contracts that are too detailed (coupling consumer and producer implementations) or too vague (missing critical assertions). The key is to specify only what the consumer actually uses. If the consumer only needs transactionId from a response, don't assert on other fields like timestamp or internalStatus.
Setting Up Spring Cloud Contract
Let's get practical. You'll need two modules: a producer and a consumer. For the producer, add the Spring Cloud Contract Verifier plugin and the spring-cloud-starter-contract-verifier dependency. For the consumer, add spring-cloud-starter-contract-stub-runner.
Producer setup (pom.xml): ``xml <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>4.1.0</version> <extensions>true</extensions> <configuration> <baseClassForTests>com.example.BaseTestClass</baseClassForTests> </configuration> </plugin> ``
Place your contracts in src/test/resources/contracts/. By default, the plugin generates tests in target/generated-test-sources/contracts/. You need a base test class that sets up the Spring context (e.g., with @SpringBootTest and @AutoConfigureMockMvc).
Consumer setup: Add the stub runner dependency and configure it to download stubs from a repository. In your test, use @AutoConfigureStubRunner with the stub coordinates.
``java @SpringBootTest @AutoConfigureStubRunner( stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:payment-service:+:stubs:8090" ) class PaymentConsumerTest { // your test using WireMock stubs } ``
This will start a WireMock server on port 8090 that serves the stubs.
stubsMode = REMOTE in CI. I've seen builds pass because they used local stubs that were out of sync with the published ones.@AutoConfigureStubRunner.Writing Contracts with Groovy DSL
Spring Cloud Contract offers two DSLs: Groovy and Java. I prefer Groovy for its conciseness, but Java works if you're allergic to Groovy. The Groovy DSL uses a Contract.make block with request and response sections.
Key elements: - description: Human-readable description of the contract. - request: Defines HTTP method, URL, headers, query params, and body. - response: Defines status, headers, and body. - bodyMatchers: For dynamic values like timestamps or IDs.
Example with body matchers: ``groovy Contract.make { description "Create payment" request { method ``POST() url "/payments" headers { contentType(applicationJson()) } body([ amount: 100.00, currency: "USD" ]) } response { status CREATED() headers { contentType(applicationJson()) } body([ id: "auto-generated", status: "PENDING", createdAt: "2024-01-01T00:00:00Z" ]) bodyMatchers { jsonPath('$.id', byRegex('[a-f0-9]{24}')) jsonPath('$.createdAt', byRegex('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z')) } } }
Notice we use byRegex to match dynamic values without hardcoding them. This is critical for avoiding brittle tests.
createdAt timestamp was one second off. Use byRegex to match the format, not the exact value.Contract Testing for Message-Based Services
Spring Cloud Contract isn't just for HTTP; it supports messaging (e.g., RabbitMQ, Kafka). The concept is the same: consumer defines the expected message, producer verifies it can produce it.
Messaging contract example: ``groovy Contract.make { description "Payment processed event" label 'payment_processed' input { triggeredBy('processPayment()') } outputMessage { sentTo('payment.events') body([ transactionId: "txn-789", status: "COMPLETED" ]) headers { header('contentType', 'application/json') } } } ``
Here, triggeredBy calls a method that triggers the message. The producer must implement processPayment() in the test base class. The consumer uses the stub to simulate receiving the message.
Consumer test: ```java @SpringBootTest @AutoConfigureStubRunner(ids = "com.example:payment-service:+:stubs:8090") class PaymentEventListenerTest {
@Autowired private StubFinder stubFinder;
@Test void shouldReceivePaymentProcessedEvent() { // trigger the stub stubFinder.trigger("payment_processed"); // assert that the message was consumed } } ```
Messaging contracts are powerful but tricky. The producer must ensure the triggeredBy method is present in the base test class.
triggeredBy and outputMessage.What the Official Docs Won't Tell You
After years of using Spring Cloud Contract in production, here are the gotchas that the official documentation glosses over:
- Stub versioning is critical. The official docs show
+for the version, which pulls the latest. In production, always pin to a specific version. Otherwise, a consumer's build can break unexpectedly when a new producer version is published. Use a property likepayment-service.versionand update it consciously. - Contract priority matters. If you have multiple contracts for the same endpoint (e.g., different responses for different states), use the
priorityfield. Higher priority contracts are matched first. Without priorities, the framework picks the first matching contract, which may not be the one you expect. - Base class must be in a specific package. The generated tests look for the base class in the same package as the contracts (or a default). If you put it in a different package, the plugin won't find it. Use
<basePackageForTests>in the plugin configuration to override. - Stub runner restarts for each test class. By default, each test class gets a new WireMock server. This can slow down your test suite. Use
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "...", port = "8090")to share a single port across tests, but be careful with state. - Messaging contracts require extra configuration. For Kafka, you need to add
spring-cloud-starter-contract-stub-runner-kafkaand configure the embedded Kafka. The docs show a simple example, but in production you'll need to handle schema registry, Avro, or custom serializers. - The plugin generates tests with a fixed naming convention. If you have multiple contracts for the same endpoint, the generated test names may collide. Use
contractsDirectoryto organize contracts in subdirectories, which generates separate test classes.
mvn clean and using -U to force snapshot updates.Integrating with CI/CD Pipeline
Spring Cloud Contract shines when integrated into your CI/CD pipeline. Here's the workflow I recommend:
- Producer pipeline:
- - Build and run unit tests.
- - Run contract verification (
mvn clean installincludes it). - - If verification passes, publish the stubs to a repository (e.g., Artifactory). Use the Maven deploy plugin with classifier
stubs. - Consumer pipeline:
- - Build and run unit tests.
- - Run consumer tests against the latest stubs from the repository.
- - If tests pass, proceed to deployment.
Key configuration: ``xml <!-- Producer: publish stubs --> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <configuration> <contractsMode>REMOTE</contractsMode> <baseClassForTests>com.example.BaseTestClass</baseClassForTests> </configuration> </plugin> ``
Consumer: use remote stubs ``java @AutoConfigureStubRunner( stubsMode = StubRunnerProperties.StubsMode.REMOTE, repositoryRoot = "https://repo.example.com/libs-release-local", ids = "com.example:payment-service:1.0.0:stubs:8090" ) ``
This setup ensures that consumers always test against the latest verified contracts. If a producer breaks a contract, the consumer's build fails before deployment.
The Silent Breaking Change That Took Down Payments
transactionId to txnId in the JSON response. No contract was in place, so the change went unnoticed.- Always version your contracts — breaking changes require major version bumps.
- Run contract verification as part of the producer's CI pipeline, not just locally.
- Consumers should test against the latest published stubs, not a shared test environment.
- Automate stub publication to a repository (e.g., Artifactory) to ensure consistency.
- Don't rely on endpoint URLs alone; the contract defines the API contract, not the URL.
name or priority. Use priority to override base contracts.mvn spring-cloud-contract:convert -DskipTestsmvn test -Dspring.cloud.contract.verifier.skip=false| File | Command / Code | Purpose |
|---|---|---|
| contract.groovy | Contract.make { | What is Consumer-Driven Contract Testing? |
| BaseTestClass.java | @SpringBootTest | Setting Up Spring Cloud Contract |
| payment_contract.groovy | Contract.make { | Writing Contracts with Groovy DSL |
| PaymentEventListenerTest.java | @SpringBootTest | Contract Testing for Message-Based Services |
| pom.xml | What the Official Docs Won't Tell You | |
| Jenkinsfile.groovy | pipeline { | Integrating with CI/CD Pipeline |
Key takeaways
Interview Questions on This Topic
Explain how Spring Cloud Contract generates tests from a Groovy contract.
src/test/resources/contracts/. For each contract, it generates a JUnit test class in target/generated-test-sources/contracts/. The test uses RestAssured to send a request matching the contract's request part and asserts that the response matches the contract's response part. The base test class provides the Spring context.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Cloud. Mark it forged?
4 min read · try the examples if you haven't