Spring Cloud: Contract

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 of spring-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.

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

spring cloud contract stubs jar

Our target folder, apart from the regular builds, now contains a stubs jar too:

stubs jar

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.