The microservices architecture allows us to scale our services efficiently and also accelerate the development of new features, but it also introduces new challenges. One of them is testing.
Microservices communicate with each other using some protocol—most commonly REST APIs—and to reduce Time to Market, it's important to parallelize development. Each microservice may be created by a different team, and the goal is to ensure they communicate well and deliver a fully functioning product without requiring many adjustments. If we use an analogy of an assembly line, different teams build parts of a product in parallel. Each team follows precise specifications to ensure compatibility and allow smooth assembly without further adjustments.
How can we test each microservice’s business logic and ensure nothing breaks in the communication between them?
Controlled environment
The first idea that comes to mind is to have a controlled development environment to test our microservice (SUT).

I was working on a team that consumed different services from an eCommerce platform and we relied on many services from different domains. In this case, we used a mock server to emulate the behavior of APIs from other domains to check if they met our acceptance criteria. This gave us confidence and could also be used in CI/CD.
It was too good to be true. We're talking about an eCommerce platform that uses a service to return a list of products. We defined the contract, published it on the Mock Server, and started development.

While developing, we realized that the productId in the product list was redundant and could be simplified to just id. The same applied to productName, which could just be name.
We spoke with the team developing the service and agreed on the changes:

We generated the contract and updated the stub servers.
Months later, expansion into new markets was approved, where the currency wasn’t the euro. Therefore, the price field, previously a string, was split into amount and currency. This was foreseeable, but we prioritized product launch, knowingly accepting this trade-off.

As before, we generated the contract and updated the stub servers. We were facing many trade-offs and this approach was slowing us down. We needed something to simplify and automate this process. That’s when I discovered Consumer-Driven Contract Testing.
Consumer-Driven Contract Testing
Consumer-Driven Contract Testing is a type of test that ensures a provider meets the expectations of the consumer. For HTTP APIs (and other synchronous protocols), this means checking that the provider accepts the expected requests and returns the expected responses. For a message queue system, it means verifying that the provider generates the expected message.
There are various tools for Contract Testing such as Pact, Spring Cloud Contract, and more.
In this article, we’ll use Spring Cloud Contract to create a producer and a consumer with Spring Boot 3, define a contract between them, and implement new features ensuring each party fulfills its part of the contract.
Terminology
- Producer. This is the microservice that exposes an API or interface to be consumed by other services. It is responsible for providing data or functionalities to the consumers. In our case, it would be the catalog domain service (catalog).
- Consumer. This is the microservice that consumes the API or interface provided by the producer. In this case, it would be the core domain of our e-commerce platform (shop).
- Contract. It’s an agreement between the consumer and the producer on how communication should be structured between them. It defines:
- The format of the request and response (JSON, XML, etc.).
- Expected fields and their data types.
- Expected HTTP status codes.
The contract is generated by the consumer and validated against the producer to ensure API changes do not break compatibility.
Spring Cloud Contract
Spring Cloud Contract is a project that provides solutions to successfully implement the Consumer-Driven Contract Testing approach. It is currently part of Spring Cloud Contract Verifier, a tool designed to facilitate contract verification in JVM-based applications.
Spring Cloud Contract Verifier enables the development of contract-based applications defined by consumers. It offers a Contract Definition Language (DSL), which can be written in Groovy or YAML. From these definitions, the tool automatically generates several resources to ensure compatibility between services:
- WireMock (HTTP Server Stub) uses default JSON stub definitions to perform integration tests in the client code (client-side tests). The test code must be written manually; Spring Cloud Contract Verifier generates the test data.
- Messaging routes (if applicable). It integrates with Spring Integration, Spring Cloud Stream, and Apache Camel. However, custom integrations can also be configured as needed.
- Acceptance tests (default in JUnit or Spock) are used to verify if the API implementation on the server side complies with the contract (server-side test). Spring Cloud Contract Verifier generates the full test.
Spring Cloud Contract Verifier brings TDD to the level of software architecture.

Benefits of Spring Cloud Contract
From the producer side:
- Great DSL to define contracts.
- Generates stubs.
- Generates tests from the contract definition.
From the consumer side:
- Simulates producers.
- Tests real communication between services.
- Validation against contracts.
- Executes stubs generated by the producer using StubRunner, which allows automatic download of dependency stubs (or loading them from the classpath), starts WireMock servers, and configures them with the appropriate stub definitions to facilitate integration testing.
Workflow
When working with Consumer-Driven Contract Testing, there are well-defined tasks to complete the process.
- Contract definition: the first step is for both teams to meet and define the contract. The consumer team defines its requirements, and together they agree on a contract.
- Stub generation and publishing: the provider team generates the stubs in their project based on the contract defined in step 1 and publishes them to a repository (Artifactory or Maven), from which the consumer team can retrieve them.
- Parallel development: both teams begin working in parallel on implementing the business logic.
What happens if one team needs to change the contract?
Raise your hand 🙋 and call a meeting with the other team to refine the contract (like in step 1). Then proceed again with steps 2 and 3 using the updated contract version.
Hands-on: practical example
We’re going to create a multi-module Maven project with 2 modules:
- catalog, which will contain our producer.
- shop, which will be the consumer project.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.paradigmadigital</groupId>
<artifactId>spring-boot-3-consumer-driven-contract-testing</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>catalog</module>
<module>shop</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
Create the Producer
Let’s head over to Josh Long's favorite page: Spring Initializr and generate the producer project including the following dependencies:
- Spring Web
- Contract Verifier
- Spring Data JPA
- H2 Database
- Lombok

We generate the project and extract its contents into the catalog module.
Create the Contracts
Inside the path catalog/src/test/resources/ we will have a contracts folder. If it doesn’t exist, create it, as we’ll be placing the contracts there. You can write them in Java or use one of the DSLs. Spring Cloud Contract supports two types of DSL: one written in Groovy and another in YAML. Let’s take a look at how to write contracts in each of the supported formats.
The contract will be based on the example we saw earlier: when a GET request is made to the /products endpoint, it will return the list of products defined in the products.json file:
{
"products": [
{
"productId": "17035535",
"productName": "Cheap product",
"price": "9.99 EUR"
},
{
"productId": "17005954",
"productName": "Quality product",
"price": "29.99 EUR"
}
]
}
Contract definition in Java:
public class shouldReturnProducts implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(contract -> {
contract.description("Should return products list");
contract.name("Java contract");
contract.request(request -> {
request.url("/products");
request.method(request.GET());
});
contract.response(response -> {
response.status(response.OK());
response.headers(header -> {
header.contentType(header.applicationJson());
});
response.body(response.file("response/products.json"));
});
}));
}
}
Contract definition in Groovy:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return products list"
name "Groovy contract"
request {
method GET()
url '/products'
}
response {
status OK()
headers {
contentType(applicationJson())
}
body(file("response/products.json"))
}
}
Contract definition in YAML:
description: yaml contract
request:
method: GET
url: /products
response:
status: 200
headers:
Content-Type: application/json
bodyFromFile: response/products.json
All 3 contracts generate the same Java code:
@Test
public void validate_shouldReturnProducts() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.get("/products");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).array("['products']").contains("['productId']").isEqualTo("17035535");
assertThatJson(parsedJson).array("['products']").contains("['productName']").isEqualTo("Cheap product");
assertThatJson(parsedJson).array("['products']").contains("['price']").isEqualTo("9.99 EUR");
assertThatJson(parsedJson).array("['products']").contains("['productId']").isEqualTo("17005954");
assertThatJson(parsedJson).array("['products']").contains("['productName']").isEqualTo("Quality product");
assertThatJson(parsedJson).array("['products']").contains("['price']").isEqualTo("29.99 EUR");
}
We can see that we have automatically generated tests based on the contract. That’s impressive.
Configure Contract Tests
We still need to configure the main class so that contract tests can run correctly. In our case, we will need:
- The two SQL scripts:
- One to create the schema.
- Another to insert the test data.
- We also need to configure RestAssuredMockMvc, which integrates Rest Assured with Spring’s MockMvc, allowing you to perform REST API tests directly on Spring MVC controllers without needing to deploy the application to a real server.
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Sql({"/sql/schema_test.sql", "/sql/test_products.sql"})
public class BaseTestClass {
@Autowired
private ProductController productController;
@BeforeEach
public void setup() {
StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(productController);
RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
}
}
Once the BaseTestClass.java is created, we need to configure it to work with the Maven plugin for Spring Cloud Contract.
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>4.2.1</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>com.paradigmadigital.catalog.BaseTestClass</baseClassForTests>
</configuration>
</plugin>
Generate the stubs
With this setup, we can now generate the stubs without having implemented the business logic using the following command:
mvn clean install -pl catalog
Ideally, the stubs should be published to Artifactory as part of the CI/CD pipeline. This way, both teams can work in parallel.
Create the consumer
We go back to Spring Initializr and generate the consumer including the following dependencies:
- Spring Web
- Contract Stub Runner
- Lombok

We generate the project and extract its contents into the shop module.
Consumer integration tests
The consumer integration tests use the stubs generated by the producer team. This is the simplest way to create an integration test:
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.paradigmadigital:catalog:+:stubs:8080")
class ShopControllerIntegrationTest {
@Autowired
private ShopController shopController;
@Test
void testContractToCatalogProducer() {
var result = shopController.getProducts().getBody();
assertNotNull(result);
assertThat(result.products()).extracting("id")
.contains(17035535L, 17005954L);
}
}
With the annotation AutoConfigureStubRunner we configure:
- StubsMode defines how the stubs are used and can have the following values:
- CLASSPATH: will use stubs from the classpath.
- LOCAL: will retrieve stubs from the local .m2 repository.
- REMOTE: will retrieve stubs from a remote location.
- ids follow the structure groupId:artifactId:version:classifier:port and specify:
- groupId of the stub artifact.
- artifactId of the stub artifact.
- version of the artifact. This can be an exact version number (e.g., 1.0.0), or + for the latest available version.
- classifier: suffix for the stub jar file (in our case it’s "stubs", since the jar file is named catalog-0.0.1-SNAPSHOT-stubs.jar). The default value is stubs.
- the port on which the WireMock server will run with the generated stubs, which in our example would be 8080.
Contract change
The team responsible for the consumer (Shop Team) is almost done with the implementation, but a potential improvement in the contract has been identified. Redundant fields have been removed:

They agreed on these changes with the team in charge of the producer (Catalog Team), updated the contract, and published a new version of the stubs. Both teams continued with the development.
The same process was followed when the price field, previously a string, was split into amount and currency.

Conclusions
We’ve seen how Spring Cloud Contract simplifies contract management between a consumer and a service provider, enabling the implementation of changes without the fear of breaking integrations.
Thanks to Spring Cloud Contract Verifier and Stub Runner, we get fast feedback without needing to deploy the full microservices ecosystem. This ensures that the stubs used during client development accurately reflect the actual server behavior.
You can view all the code with step-by-step commits in the GitHub repository: Example of Consumer Driven Contract Testing with Spring Boot 3.
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.