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).

system under test --> mock server --> service 1 / service 2 / service 3

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.

"products": { "productId": "17035535", "productName": "Cheap product", "price": "9.99 EUR" { "productId": "17005954", "productName": "Quality product" "price": "29.99 EUR"

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:

Optimized product list replacing "productId" with "id" and "productName" with "name"

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.

Previously, price was "9.99 EUR". The updated version splits price into "amount": "9.99" and "currency": "EUR"

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

  1. 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).
  2. 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).
  3. Contract. It’s an agreement between the consumer and the producer on how communication should be structured between them. It defines:

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:

Spring Cloud Contract Verifier brings TDD to the level of software architecture.

Spring Cloud Contract Verifier structure

Benefits of Spring Cloud Contract

From the producer side:

From the consumer side:

Workflow

When working with Consumer-Driven Contract Testing, there are well-defined tasks to complete the process.

  1. 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.
  2. 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.
  3. 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:

<?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 Initializr configuration page including 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:

@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 initializr configuration page including 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:

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:

screenshot showing changes for code optimization (id / name)

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.

code showing the changes made to "price": 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.

Tell us what you think.

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.

Subscribe