- Published on
How to Test Your Microservices with Spring Cloud Contracts
- Authors
- Name
- Usman Akhtar
- @usmanakhtar
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.
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.