Contract testing with Spring
Efficient distributed software testing, in particular microservices, is a very challenging subject, yet essential to success. Therefore, as programmers, we often confront a task: how to effectively simulate the behavior of an external service? As we want to quickly receive feedback when changes on either end are causing issues and how they propagate. In this article I present a standardized approach to the problem, a step-by-step guide on how to create effective test infrastructure for your services using Spring Cloud Contract.
If you would like to preview the code used in this article, just clone this repository.
Contract Testing
So before we jump into code, let’s start with a few definitions. I know, I know, who needs a theory, ey? But I just want to ensure that it’s fully clear what the subject is.
The Contract is a formal, precise and verifiable interface specification for software components. Similarly, like in business it defines conditions and obligations for participating sides, which typically are available interactions, message structures and communication protocols. The participating sides are in this case software pieces, often referred to as Producer and Consumer.
As a Producer we can view the Contract as an expectation on what data or actions shall we provide and how. On the other side, Consumers (services that uses the data prepared by Producer) use the Contract to understand how to interact with it and what responses to expect.
Contract Testing is an approach where Contracts become part of the source code and are used to test it. The goal is to ensure compatibility via fast detection of breaches. As those may occur on either side. Typically the Contract is published as a shareable library, so both Producer and Consumer can rely on it and comply with the agreed structure.
Contract Tests are not aimed at business features testing or simulate full behavior, they are supposed to only verify the Contract. Thanks to that they allow us to quickly verify the service using stubs without the need to perform time-consuming end-to-end tests, which can be a huge (timewise) win in case of bigger applications.
Spring Cloud Contract
Ok, so let’s also get a little background about the Spring Cloud Contract project and outline how we will use it.
As we can read from the documentation, Spring Cloud Contract is an umbrella project holding solutions that help users in successfully implementing the Consumer Driven Contract. In the Spring Cloud Contract nomenclature, we call a pair of such services as the Producer who supplies the service and the Consumer who uses it.
For the purposes of this article, we will create these services and define a Contract that specifies how they communicate. On the basis of the mentioned Contract, after each change in the application tests and stubs will be regenerated and will guarantee quick feedback if the Contract between the services become broken. When testing microservices, this is a much better approach than creating mocks manually (e.g. using MockRestServiceServer
) because it provides stubs created directly by the service we are calling. This means that they have also been tested on the producer side. What’s more, in Spring Cloud Contract, the creation and release of stubs by the Producer is enforced after every change in service. It means that our stubs are always up to date and we can trust them.
Producer
Now it’s time for practice! Let’s start with the Producer.
Therefore, we will start by creating a simple Spring Boot web application and adding the following dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
For the sake of simplicity, the Producer will be a service with just one endpoint that sums up 2 provided numbers. So let’s create a controller:
@RestController
public class MathController {
@GetMapping("/sum")
public int sumNumbers(@RequestParam("argA") Integer n1,
@RequestParam("argB") Integer n2) {
return n1 + n2;
}
}
Now that we have a working endpoint, let’s create a Contract with a self-descriptive name shouldReturnSumOfGivenNumbers.groovy
(if you prefer the TDD approach, you can first create a contract and generate stubs, and then move on to implementation). To define a contract we call Contract.make
and then:
- Define the consumer’s request details
- Type of method
GET
- Target endpoint
/sum
- Query parameters
argA
&argB
- Type of method
- Define the producer’s expected response with
status 200
and body5
.
package contracts
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return sum of argA and argB"
request {
method GET()
url("/sum") {
queryParameters {
parameter("argA", "2")
parameter("argB", "3")
}
}
}
response {
body("5")
status 200
}
}
If you prefer a more declarative style, then it’s also possible to define contracts using yaml files. The contract’s default path is src/test/resources/contracts
but it’s possible to overwrite it by setting contractsDslDir
property in pom.xml
or build.gradle
file.
Based on the above Contract, we can automatically generate stubs and verification tests during the application building process. For that we use the spring-cloud-contract-maven-plugin:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.1.1.RELEASE</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>
engineering.iterative.producer.setup.ProducerTestSetup
</baseClassForTests>
</configuration>
</plugin>
In section <baseClassForTests>
we provided the path to the configuration class of our tests, which is ProducerTestSetup
. If you prefer you can also use gradle with spring-cloud-contract-gradle-plugin:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
}
}
The ProducerTestSetup looks as follows, note that each generated test will inherit from this class.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMessageVerifier
public class ProducerTestSetup {
@Autowired
private MathController mathController;
@Before
public void setup() {
StandaloneMockMvcBuilder standaloneMockMvcBuilder =
MockMvcBuilders.standaloneSetup(mathController);
RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
}
}
The MockMvcBuilders.standaloneSetup()
allows the registration of MathController
without the need to use the full web application context. RestAssuredMockMvc
is a REST-assured API built on top of Spring’s MockMvc.
When we run the build by calling mvn clean install
, the plugin automatically generates a test class in /target/generated-test-sources/contracts/ directory:
public class ContractVerifierTest extends ProducerTestSetup {
@Test
public void validate_shouldReturnSumOfGivenNumbers() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.queryParam("argA","2")
.queryParam("argB","3")
.get("/sum");
// then:
assertThat(response.statusCode()).isEqualTo(200);
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
String responseBody = response.getBody().asString();
assertThat(responseBody).isEqualTo("5");
}
}
That’s it – the Producer setup is done and ready to be used by Consumers.
Consumer
Since we already have a Producer implementation and defined contract, let’s move on to creating the Consumer service. Our new service will be responsible for calculating the average value of 2 given numbers.
To do this, we first need to use a producer’s endpoint to provide us the sum of the mentioned numbers, and then we can return their average value.
@RestController
@RequiredArgsConstructor
public class CalculationController {
private final RestTemplate restTemplate;
@GetMapping("/average")
public Double averageOfTwoDigits(@RequestParam("argA") Integer argA,
@RequestParam("argB") Integer argB) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Type", "application/json");
ResponseEntity<String> responseEntity = restTemplate.exchange(
"http://localhost:8090/sum?argA=" + argA + "&argB=" + argB,
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
String.class);
double sumResult = Double.parseDouble(Objects.requireNonNull(responseEntity.getBody()));
return sumResult/2.0;
}
}
We also need to add the spring-cloud-contract-wiremock and spring-cloud-contract-stub-runner dependencies:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
The last step is to create an integration test for the Consumer. This time, we will not mock the behavior of remote service in the test, but we will use generated by the producer stubs. For this purpose, we will use the @AutoconfigureStubRunner
annotation, in which we indicate the artifactId of the jar file that contains our stubs. For this demo project I use LOCAL
stubs mode, but typically REMOTE
is the state we want to reach. You can read more about stubs modes here.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "engineering.iterative:producer")
class CalculationControllerTest {
@Autowired private MockMvc mockMvc;
@Test
public void given_WhenPassEvenNumberInQueryParam_ThenReturnEven() throws Exception {
mockMvc
.perform(
MockMvcRequestBuilders.get("/average?argA=2&argB=3")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string("2.5"));
}
}
To create the CalculationControllerTest
we used several Spring annotations like @RunWith
, @SpringBootTest
or @AutoConfigureMockMvc
. If you don’t know them yet, just read this article.
After running this test, we can see that it uses the generated stubs:
WireMock: Admin request received:
127.0.0.1 - POST /mappings
Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/11.0.15)]
Host: [localhost:12003]
Content-Length: [411]
Content-Type: [text/plain; charset=UTF-8]
{
"id" : "1fbe1b09-2485-4add-a25e-49471f45e665",
"request" : {
"urlPath" : "/sum",
"method" : "GET",
"queryParameters" : {
"argA" : {
"equalTo" : "2"
},
"argB" : {
"equalTo" : "3"
}
}
},
"response" : {
"status" : 200,
"body" : "5",
"transformers" : [ "response-template" ]
},
"uuid" : "1fbe1b09-2485-4add-a25e-49471f45e665"
}
o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs
[namesAndPorts={engineering.iterative:producer:0.0.1-SNAPSHOT:stubs=12003}]
Now, when we change something on the Producer’s side, we also have to change the Contract. After it is pushed to the common repository (in our example – local repository), the Consumer’s tests will stop passing due to the fact that it does not meet the new conditions of the contract.
Conclusions
Spring Cloud Contract allows to easily implement a contract driven approach, which provides autocontrol over API changes. It is especially useful when dealing with microservices architecture, benefiting with:
- Quick feedback, on both ends, if breaking changes are applied to a Contract
- No need to mock the behavior of remote services in consumer’s tests
- Formalizing the way and place that Contracts are defined. All you need to do is to define a contract and release mocks so that other teams can start working on their services before we even start implementing the producer’s logic.
- In setups where given service is used by many other services Stubs are always up to date (new stubs release is enforced after every change in Producer side)
If you found this article interesting check out official documentation, take a look at videos by Spring Cloud Contract authors or talk to us.