Spring Cloud Contract: Contract Testing for Java Microservices
Spring Cloud Contract is the contract testing framework for Java Spring applications. Unlike Pact (which is consumer-driven), Spring Cloud Contract is provider-driven — the provider defines contracts in the Groovy DSL, generates WireMock stubs from them, and consumers use those stubs in their tests. This guide shows how to implement Spring Cloud Contract in a Java microservice architecture.
Provider-Driven vs. Consumer-Driven Contracts
Pact (consumer-driven): Consumers write the contracts. Providers verify them. Consumers control what the provider must support.
Spring Cloud Contract (provider-driven): Providers write the contracts. Consumers use auto-generated stubs. Providers control what they guarantee to consumers.
Both approaches work. Spring Cloud Contract fits better when:
- The provider is a well-defined API that multiple consumers depend on
- The provider team writes the contracts alongside their implementation
- You're in a Java/Spring ecosystem and want tight Maven/Gradle integration
Pact fits better when:
- Consumers are diverse (multiple languages, multiple teams)
- You want consumers to drive what the provider must implement
- You need a language-agnostic broker
Project Setup
Provider (Spring Boot application):
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>
com.example.userservice.ContractTestBase
</baseClassForTests>
</configuration>
</plugin>
</plugins>
</build>Consumer (another Spring Boot application):
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Writing Contracts (Provider Side)
Contracts live in src/test/resources/contracts/ in the provider project. Use the Groovy DSL:
// src/test/resources/contracts/users/get_user_by_id.groovy
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Get user by ID - user exists"
request {
method 'GET'
url '/users/123'
headers {
accept('application/json')
}
}
response {
status 200
headers {
contentType('application/json')
}
body([
id: 123,
name: "Alice Smith",
email: "alice@example.com",
active: true
])
}
}For dynamic values, use matchers:
Contract.make {
description "Get any existing user"
request {
method 'GET'
url $(consumer(regex('/users/[0-9]+')), producer('/users/123'))
}
response {
status 200
body([
id: $(producer(123), consumer(anyPositiveInt())),
name: $(anyNonBlankString()),
email: $(consumer(anyEmail()), producer('alice@example.com')),
active: $(anyBoolean())
])
headers {
contentType('application/json')
}
}
}consumer() specifies what the generated WireMock stub uses. producer() specifies what the provider verification test uses.
For POST requests with request bodies:
Contract.make {
description "Create a new user"
request {
method 'POST'
url '/users'
headers {
contentType('application/json')
}
body([
name: $(anyNonBlankString()),
email: $(consumer(anyEmail()), producer('newuser@example.com'))
])
bodyMatchers {
jsonPath('$.name', byType())
jsonPath('$.email', byRegex(email()))
}
}
response {
status 201
headers {
contentType('application/json')
}
body([
id: $(anyPositiveInt()),
name: fromRequest().body('$.name'),
email: fromRequest().body('$.email')
])
}
}Provider Test Setup
Spring Cloud Contract generates JUnit 5 tests from contracts automatically. You provide a base test class with setup:
// src/test/java/com/example/userservice/ContractTestBase.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public abstract class ContractTestBase {
@LocalServerPort
int port;
@Autowired
UserRepository userRepository;
@BeforeEach
void setup() {
RestAssured.port = port;
// Set up test data that contracts expect
userRepository.deleteAll();
userRepository.save(new User(123L, "Alice Smith", "alice@example.com", true));
}
}Running mvn test in the provider project:
- Reads contracts from
src/test/resources/contracts/ - Generates test classes that extend your base class
- Executes the tests against your running Spring Boot app
- Verifies response matches contract
Generated test (for reference — you don't edit this):
// target/generated-test-sources/contracts/com/example/ContractVerifierTest.java
public class ContractVerifierTest extends ContractTestBase {
@Test
public void validate_get_user_by_id() throws Exception {
// given
MockMvcRequestSpecification request = given()
.header("Accept", "application/json");
// when
ResponseOptions response = given().spec(request)
.get("/users/123");
// then
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['id']").isEqualTo(123);
assertThatJson(parsedJson).field("['name']").isEqualTo("Alice Smith");
assertThatJson(parsedJson).field("['email']").isEqualTo("alice@example.com");
}
}Publishing Stubs (Maven)
After contracts are verified, publish the generated stubs to a Maven repository:
mvn install # Installs stubs to local Maven repo
<span class="hljs-comment"># or
mvn deploy <span class="hljs-comment"># Publishes to remote repository (Nexus, Artifactory)The plugin generates a user-service-stubs.jar containing WireMock stubs for each contract.
Consumer Tests Using Stubs
Consumers use @AutoConfigureStubRunner to automatically start stub servers:
// order-service/src/test/java/com/example/orderservice/UserClientTest.java
@SpringBootTest
@AutoConfigureStubRunner(
ids = "com.example:user-service:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class UserClientTest {
@Autowired
UserClient userClient;
@Test
void getUser_returnsUserData() {
// WireMock stub running on port 8080 from user-service stubs
User user = userClient.getUser(123);
assertThat(user.getId()).isEqualTo(123);
assertThat(user.getName()).isNotBlank();
assertThat(user.getEmail()).contains("@");
}
@Test
void getUser_notFound_throwsException() {
// The 404 stub was generated from the contract
assertThatThrownBy(() -> userClient.getUser(999))
.isInstanceOf(UserNotFoundException.class);
}
}ids format: groupId:artifactId:version:classifier:port
For CI, use REMOTE mode to fetch stubs from the artifact repository:
@AutoConfigureStubRunner(
ids = "com.example:user-service:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "https://nexus.example.com/repository/maven-snapshots/"
)YAML Contract Alternative
If Groovy DSL feels heavy, Spring Cloud Contract also supports YAML:
# src/test/resources/contracts/users/get_user.yml
description: "Get user by ID"
request:
method: GET
url: /users/123
headers:
Accept: application/json
response:
status: 200
headers:
Content-Type: application/json
body:
id: 123
name: Alice Smith
email: alice@example.com
matchers:
body:
- path: $.id
type: by_type
- path: $.name
type: by_regex
value: ".+"
- path: $.email
type: by_regex
value: ".+@.+"YAML contracts are easier for non-Java developers to read and write.
Messaging Contracts
Spring Cloud Contract supports message-based interactions (Kafka, RabbitMQ) in addition to REST:
Contract.make {
description "Order created event"
label 'order_created'
input {
triggeredBy('createOrder()')
}
outputMessage {
sentTo('order-events')
body([
orderId: $(anyPositiveInt()),
userId: $(anyPositiveInt()),
total: $(anyPositiveDecimalNumber()),
status: 'CREATED'
])
headers {
header('contentType', 'application/json')
}
}
}This verifies that when createOrder() is called on the provider, it publishes a message matching the contract to the order-events topic.
CI/CD Integration
# Provider pipeline
provider-contract-tests:
stage: test
script:
- mvn test # Runs contract verifier tests
- mvn deploy -DskipTests # Publishes stubs to artifact repo
# Consumer pipeline
consumer-integration-tests:
stage: test
script:
- mvn test # Uses stubs from artifact repo via @AutoConfigureStubRunnerComparing with Pact for Java
Spring Cloud Contract also has a Pact integration — you can write contracts in Pact format and use the Pact Broker while still in the Spring ecosystem. This is useful when you have polyglot microservices and need a language-agnostic broker.
| Spring Cloud Contract | Pact | |
|---|---|---|
| Driven by | Provider | Consumer |
| Contract format | Groovy DSL / YAML | JSON (pact file) |
| Stub format | WireMock JSON | N/A (mock embedded) |
| Distribution | Maven artifact | Pact Broker |
| Messaging | Native (Kafka, AMQP) | Limited |
| Language support | JVM-first | Polyglot |
For pure Java Spring shops, Spring Cloud Contract has tighter integration. For polyglot architectures, Pact with a broker is more flexible.
Summary
Spring Cloud Contract workflow:
- Provider writes contracts in Groovy DSL or YAML in
src/test/resources/contracts/ - Maven plugin generates and runs tests against the provider implementation
- Stubs published as Maven artifact after verification
- Consumers use
@AutoConfigureStubRunnerto load stubs from the artifact repository - Consumer tests run against the WireMock stub without a real provider
The Maven artifact approach makes Spring Cloud Contract work naturally in Java CI/CD pipelines — stubs version alongside the service, consumers specify the version they depend on, and incompatibilities are caught when the build fails to pull or verify stubs.