Overview
In this article, we'll introduce you to Spring Cloud Contract, which is Spring's response to Consumer-Driven Contracts.
Nowadays, applications are thoroughly tested - whether it be unit tests, integration tests, or end-to-end tests. It's very common in a microservice architecture that a service (consumer) communicates with another service (producer) to complete a request.
To test them, we have two options:
- Deploy all microservices and perform end-to-end tests using a library like Selenium
- Write integration tests by mocking the calls to other services
If we take the former approach, we would be simulating a production-like environment. This will require more infrastructure and the feedback would be late as it takes a lot of time to run.
If we take the latter approach, we would have faster feedback, but since we are mocking the outside call responses, the mocks won't reflect changes in the producer, if there are any.
For example, suppose we mock the call to an outside service that returns JSON with a key, say, name
. Our tests pass and everything is working fine. As time passes the other service has changed the key to fname
.
Our integration test cases will still work just fine. The issue will likely be noticed in a staging or production environment, instead of the elaborate test cases.
Spring Cloud Contract provides us with the Spring Cloud Contract Verifier exactly for these cases. It creates a stub from the producer service which can be used by the consumer service to mock the calls.
Since the stub is versioned according to the producer service, the consumer service can choose which version to choose for tests. This provides both faster feedback and makes sure our tests actually reflect the code.
Setup
To demonstrate the concept of contracts, we have the following back-end services:
- spring-cloud-contract-producer: A simple REST service that has a single endpoint of
/employee/{id}
, which produces a JSON response. - spring-cloud-contract-consumer: A simple consumer client that calls
/employee/{id}
endpoint ofspring-cloud-contract-producer
to complete its response.
To focus on the topic, we would be only using these service and not other services like Eureka, Gateway, etc. that are typically included in an microservice architecture.
Producer Setup Details
Let's start with the simple POJO class - Employee
:
public class Employee {
public Integer id;
public String fname;
public String lname;
public Double salary;
public String gender;
// Getters and setters
Then, we have an EmployeeController
with a single GET
mapping:
@RestController
public class EmployeeController {
@Autowired
EmployeeService employeeService;
@GetMapping(value = "employee/{id}")
public ResponseEntity<?> getEmployee(@PathVariable("id") int id) {
Optional<Employee> employee = employeeService.findById(id);
if (employee.isPresent()) {
return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(employee.get());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
}
It's a simple controller that returns an Employee
JSON with all the class attributes as JSON keys, based on the id
.
EmployeeService
could be anything that finds the employee by id
, in our case, it's a simple implementation of JpaRepository
:
public interface EmployeeService extends JpaRepository<Employee, Integer> {}
Consumer Setup Details
On the consumer side, let's define another POJO - Person
:
class Person {
private int id;
public String fname;
public String lname;
// Getters and setters
Note that the name of the class doesn't matter, as long as the attributes name are the same - id
, fname
, and lname
.
Now, suppose we have a component that calls the /employee/{id}
endpoint of spring-cloud-contract-producer
:
@Component
class ConsumerClient {
public Person getPerson(final int id) {
final RestTemplate restTemplate = new RestTemplate();
final ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8081/employee/" + id,
HttpMethod.GET, null, Person.class);
return result.getBody();
}
}
Since the Person
class from spring-cloud-contract-consumer
has the same attribute names as that of the Employee
class from spring-cloud-contract-producer
- Spring will automatically map the relevant fields and provide us with the result.
Testing the Consumer
Now, if we'd like to test the consumer service, we'd make a mock test:
@SpringBootTest(classes = SpringCloudContractConsumerApplication.class)
@RunWith(SpringRunner.class)
@AutoConfigureWireMock(port = 8081)
@AutoConfigureJson
public class ConsumerTestUnit {
@Autowired
ConsumerClient consumerClient;
@Autowired
ObjectMapper objectMapper;
@Test
public void clientShouldRetrunPersonForGivenID() throws Exception {
WireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/employee/1")).willReturn(
WireMock.aResponse()
.withStatus(200)
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
.withBody(jsonForPerson(new Person(1, "Jane", "Doe")))));
BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane");
}
private String jsonForPerson(final Person person) throws Exception {
return objectMapper.writeValueAsString(person);
}
}
Here, we mock the result of the /employee/1
endpoint to return a hardcoded JSON response and then carry on with our assertion.
Now, what happens if we change something in the producer?
The code that tests the consumer won't reflect that change.
Implementing Spring Cloud Contract
To make sure that these services are "on the same page" when it comes to changes, we provide them both with a contract, just like we would with humans.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
When the producer service gets changed, a stub/receipt is created for the consumer service to let it know what's going on.
Producer Service Contract
To implement this, first, let's add the spring-cloud-starter-contract-verifier
dependency in our producer's pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
Now, we need to define a contract based on which Spring Cloud Contract will run tests on and build a stub. This is done via the spring-cloud-starter-contract-verifier
which is shipped with Contract Definition Language (DSL) written in Groovy or YAML.
Let's create a contract, using Groovy in a new file - shouldReturnEmployeeWhenEmployeeIdFound.groovy
:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("When a GET request with an Employee id=1 is made, the Employee object is returned")
request {
method 'GET'
url '/employee/1'
}
response {
status 200
body("""
{
"id": "1",
"fname": "Jane",
"lname": "Doe",
"salary": "123000.00",
"gender": "M"
}
""")
headers {
contentType(applicationJson())
}
}
}
This is a pretty simple contract which defines a couple of things. If there's a GET
request to the URL /employee/1
, return a response of status 200
and a JSON body with 5 attributes.
When the application is built, during the test phase, automatic test classes will be created by Spring Cloud Contract that will read upon this Groovy file.
However, to make it possible for test classes to be auto-generated, we need to make a base class which they can extend. To register it as the base class for tests, we add it to our pom.xml
file:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<baseClassForTests>
com.mynotes.springcloud.contract.producer.BaseClass
</baseClassForTests>
</configuration>
</plugin>
Our BaseClass
looks something like:
@SpringBootTest(classes = SpringCloudContractProducerApplication.class)
@RunWith(SpringRunner.class)
public class BaseClass {
@Autowired
EmployeeController employeeController;
@MockBean
private EmployeeService employeeService;
@Before
public void before() {
final Employee employee = new Employee(1, "Jane", "Doe", 123000.00, "M");
Mockito.when(this.employeeService.findById(1)).thenReturn(Optional.of(employee));
RestAssuredMockMvc.standaloneSetup(this.EmployeeController);
}
}
Now, let's build our app:
$ mvn clean install
Our target
folder, apart from the regular builds, now contains a stubs
jar too:
Since we performed install
, it is also available in our local .m2
folder. This stub can now be used by our spring-cloud-contract-consumer
to mock the calls.
Consumer Service Contract
Similar to the producer side, we need to add a certain kind of contract to our consumer service too. Here, we need to add spring-cloud-starter-contract-stub-runner
dependency to our pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
Now, instead of making our local mocks, we can download the stubs from the producer:
@SpringBootTest(classes = SpringCloudContractConsumerApplication.class)
@RunWith(SpringRunner.class)
public class ConsumerTestContract {
@Rule
public StubRunnerRule stubRunnerRule = new StubRunnerRule()
.downloadStub("com.mynotes.spring-cloud", "spring-cloud-contract-producer", "0.0.1-SNAPSHOT", "stubs")
.withPort(8081)
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
@Autowired
ConsumerClient consumerClient;
@Test
public void clientShouldRetrunPersonForGivenID_checkFirsttName() throws Exception {
BDDAssertions.then(this.consumerClient.getPerson(1).getFname()).isEqualTo("Jane");
}
@Test
public void clientShouldRetrunPersonForGivenID_checkLastName() throws Exception {
BDDAssertions.then(this.consumerClient.getPerson(1).getLname()).isEqualTo("Doe");
}
}
As you can see, we used the stub created by spring-cloud-contract-producer
. The .stubsMode()
is to tell Spring where it should look stub dependency. LOCAL
means in the local .m2
folder. Other options are REMOTE
and CLASSPATH
.
The ConsumerTestContract
class will run the stub first and because of its provider by the producer, we are independent of mocking the external call. If suppose the producer did change the contract, it can be quickly found out from which version the breaking change was introduced and appropriate steps can be taken.
Conclusion
We've covered how to use Spring Cloud Contract help us maintain a contract between a producer and consumer service. This is achieved by first creating a stub from the producer side using a Groovy DSL. This generated stub can be used in the consumer service to mock external calls.
As always, the code for the examples used in this article can be found on GitHub.