Published on

How to Test Your Microservices with Spring Cloud Contracts

Authors
spring-cloud-header

Contract Testing

Contract testing is a method of testing software systems that focuses on the interactions between different microservices or components within a larger system. The goal of contract testing is to ensure that each microservice or component meets its expected behavior and obligations according to a predefined set of expectations, which are typically documented in a contract.

If you want to read more about contract testing read an article Contract Testing: Importance in Micro services Ecosystem which takes more in-depth approach to it.

Spring Cloud Contracts

Spring Cloud Contracts is a framework that provides a convenient way for Java developers to perform contract testing in their Spring Boot applications. By leveraging Spring Cloud Contracts, development teams can define contracts that outline the expected behavior of their services, and then generate test cases automatically based on those contracts. This eliminates the need for developers to manually create test cases, reducing the time and effort required to perform thorough testing.

Implementation

Let's consider a simple example to grasp the workings of Spring Cloud. We have two services: employee-service and department-service. The employee-service has an endpoint that retrieves employee details, including the department they belong to. To obtain the department information, the employee-service interacts with the department-service.

spring-cloud-example

Producer

In contract testing, the producer (also known as the provider) is responsible for creating and exposing an API while defining the API contract. In the case of our example, the department-service is currently functioning as the producer. Now, let's delve into the process of creating it.

Department Service

Begin by creating an empty Maven project and incorporating the following dependencies into it.

<!-- Spring cloud contract -->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-verifier</artifactId>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>com.fasterxml.jackson.module</groupId>
   <artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
   <groupId>org.jetbrains.kotlin</groupId>
   <artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
   <groupId>org.jetbrains.kotlin</groupId>
   <artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Next, let's add these plugins as well. In particular, note that the baseClassForTests plugin includes the path to the BaseClass that configures the contract test. We will explore this further in the subsequent steps.


	<!-- Spring cloud contract -->
	<plugin>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-contract-maven-plugin</artifactId>
      <version>3.1.7</version>
      <extensions>true</extensions>
      <configuration>
         <testFramework>JUNIT5</testFramework>
         <baseClassForTests>com.codecrust.department.BaseClass</baseClassForTests>
      </configuration>
   </plugin>


	<plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
         <excludes>
            <exclude>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
            </exclude>
         </excludes>
      </configuration>
   </plugin>
   <plugin>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-maven-plugin</artifactId>
      <configuration>
         <args>
            <arg>-Xjsr305=strict</arg>
         </args>
         <compilerPlugins>
            <plugin>spring</plugin>
         </compilerPlugins>
      </configuration>
      <dependencies>
         <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlin.version}</version>
         </dependency>
      </dependencies>
   </plugin>

Let's proceed by creating a basic model called DepartmentResponse.

data class DepartmentResponse(
    val id: String,
    val name: String
)

Now let's create a simple service class called DepartmentService.

import com.codecrust.department.models.DepartmentResponse
import org.springframework.stereotype.Service

@Service
class DepartmentService {

    fun getDepartmentByEmployeeId(employeeId: String): DepartmentResponse {
        return DepartmentResponse(
            id = "department-1",
            name = "Sales"
        )
    }
}

Afterwards, let's create a simple controller that will handle the retrieval of department details.

import com.codecrust.department.models.DepartmentResponse
import com.codecrust.department.services.DepartmentService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
class DepartmentController(val service: DepartmentService) {

    @GetMapping("/department/{employeeId}")
    fun getDepartmentByEmployeeId(@PathVariable employeeId: String): DepartmentResponse{
        return service.getDepartmentByEmployeeId(employeeId)
    }
}

Now, let's create a BaseClass to configure the DepartmentController for contract testing.

import com.codecrust.department.controllers.DepartmentController
import io.restassured.module.mockmvc.RestAssuredMockMvc
import org.junit.jupiter.api.BeforeEach
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(classes = [DepartmentApplication::class])
abstract class BaseClass {
    @Autowired
    var departmentController: DepartmentController? = null

    @BeforeEach
    fun setup() {
        RestAssuredMockMvc.standaloneSetup(departmentController)
    }
}

Next, add a Groovy file in the resources/contracts directory to define the contract for the /department/{employee-id} endpoint.

import org.springframework.cloud.contract.spec.Contract

Contract.make {
description "should return sales department by employeeId employee-1"
request {
url "/department/employee-1"
method GET()
}
response {
			status OK()
			headers{
				contentType applicationJson()
			}
			body(
				id: "department-1",
				name: "Sales",
			)}
}

Now, when we install the Maven project using a command like mvn clean compile install, it will automatically generate the test class for us inside the target/generated-test-sources/contracts directory. With this, we have completed the configuration of the department-service as the producer. Now, we can proceed to create the employee-service, which will consume the /department/{employeeId} endpoint.

Consumer

In the terms of contract testing, the consumer is the entity or system that utilizes the API exposed by the producer and tests its implementation against the provided contract or specification. In our specific case, the employee-service will serve as the consumer. Now, let's commence the creation of the employee-service.

Employee Service

To begin, create an empty Maven project and incorporate the following dependencies into it:

<!-- Spring cloud contract -->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>com.fasterxml.jackson.module</groupId>
   <artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
   <groupId>org.jetbrains.kotlin</groupId>
   <artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
   <groupId>org.jetbrains.kotlin</groupId>
   <artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

Next, let's create two models: DepartmentResponse and EmployeeResponse.

data class DepartmentResponse(
    val id: String,
    val name: String
)

data class EmployeeResponse(
    val id: String,
    val name: String,
    val department: String?,
)

Now, let's create a basic EmployeeService class.

import com.codecrust.employee.models.DepartmentResponse
import com.codecrust.employee.models.EmployeeResponse
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate

@Service
class EmployeeService(val restTemplate: RestTemplate) {

    fun getEmployeeById(id: String): EmployeeResponse {
        val httpHeaders = HttpHeaders()
        httpHeaders.add("Content-Type", "application/json")
        val requestEntity = HttpEntity<Void>(httpHeaders)

        val responseEntity: ResponseEntity<DepartmentResponse> = restTemplate.exchange(
            "http://localhost:8080/department/$id",
            HttpMethod.GET,
            requestEntity,
            DepartmentResponse::class.java
)

        return EmployeeResponse(
            id="employee-id",
            name = "John",
            department = responseEntity.body!!.name
        )
    }
}

Afterward, let's create a simple controller for the employee-service.

import com.codecrust.employee.models.EmployeeResponse
import com.codecrust.employee.services.EmployeeService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
class EmployeeController(val service: EmployeeService) {

    @GetMapping("/employee/{employeeId}")
    fun getEmployeeById(@PathVariable employeeId: String): EmployeeResponse {
        return service.getEmployeeById(employeeId)
    }
}

Afterwards, let's create a simple contract test called EmployeeControllerContractTests for the EmployeeController.

import org.hamcrest.CoreMatchers.containsString
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureStubRunner(
    ids = ["com.codecrust:department:+:stubs:8100"],
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class EmployeeControllerContractTests {

    @Autowired
    private val mockMvc: MockMvc? = null

    @Test
    @Throws(Exception::class)
    fun shouldReturnDefaultMessage() {
        mockMvc!!.perform(
            get("/employee/employee-1")
        )
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("{\"id\":\"employee-id\",\"name\":\"John\",\"department\":\"Sales\"}")))
    }
}

Next, make an additional change by adding the following lines to the application.properties file.

server.port = 8081
producer.port = 8080 # port at which department-service (producer) running on

That's it! We are now ready. If we run the tests, they will verify the finalized contract and report an error if it does not match.

Failure Case:

Now, let's examine what happens if the contract is modified. To do that, navigate to the department-service and remove the id field from DepartmentResponse, DepartmentService, and get_department_by_employee_id.groovy. Afterward, run the application. Next, attempt to run the EmployeeControllerContractTests again. You will observe that the test fails because it expects the id field in the contract, which is now missing. This demonstrates how contract testing can identify inconsistencies and ensure compliance between the producer and consumer.

Boost your testing skills with the latest methods and tools, keeping your code robust and ready for success, subscribe to the free newsletter today for updates and insights delivered straight to your inbox.