Consumer Driven Contracts

Microservice integration

Organizations that move towards microservices, experience new issues that come with this way of working. Imagine it’s the last day of the sprint and you spot a really annoying typo in your field-name. What do you do?

Will you get into trouble if you change it? You could off course just roll the dice and do it. You make the change, commit it, wait for the pipeline, start the integration test set and wait again. But how much time does that cost? And even after that time are you absolutely sure that you won’t break anything on live? It could be that not all the right versions of your consumers were on the environment. And then you need to run it again.

Spring cloud contract

In this article I will show you how to solve this problem with Spring Cloud Contract, with nothing more then a little piece of code written in a Groovy DSL that looks like this:

package contracts.order

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

/**
 * Contract definition, written in a Groovy DSL
 */
Contract.make {

    /**
     * Request response which succeeds, completes HTTP status code 200
     */
    request {
        method 'GET'
        url('/orders/1')
        headers {
            accept("application/json")
        }
    }
    response {
        status 200
        body(["id" : 1])
        headers {
            contentType(applicationJson())
        }
    }
}

The problem

The situation I sketched will probably not be unfamiliar to you. But how do you solve it? The issue is actually at the contract level, that needs to be defined between consumer and provider (or API). There’s a lot of theory about contract changes and how to handle those ‘service evolution patterns’ these are called. If you will search for it you might find there are a couple well known patterns for handling api / consumer compatibility.

Dataset amendment is one of those patterns, where you use a schema specification and you extend types within the schema outside of the type. This gives you backwards compatibility, old consumers can use the old type and new clients can extend the type with the newly defined field.
Tolerant reader is another way is to implement this a client side solution where you do some kind of transformation to deal with changes of the API.
Schema versioning is the last and well known pattern, where you keep alive multiple versions of your api. So you do not break your old clients.

 But actually none of these patterns handle the problem of integrating and validating between producer and consumer. So there is another one that I would like to take into account.

Consumer Driven Contracts

Consumer Driven Contractsor or CDC for short. Consumer driven contracts could be considered test driven development at an API level. Every consumer creates a test set upfront, that they can use to build against (in the form of stubs) and that the producer can validate itself against. By sharing the consumer generated expectations and validating these within the development phase of your API, you can shorten the integration cycle and become aware of integration issues at an early moment. 

In this approach the API can not modify the contracts delivered by the consumer, but does test their code against it. This way integration is done during development. By validating against all consumers within your own build you are sure, that you will never break a feature within the application chain that is marked as important by one of your consumers.

So let’s dive into it.

On a side note; for this blog I dived into Spring Cloud Contract. Another option would be to use PACT, but since we use most of the Spring tooling within our projects I used the tools provided by Spring..

Let’s begin with the contract defined earlier in this article. Spring comes with a maven plugin that will render Stubs for the consumer and generated test for testing the API behaviour. So what does it generate?

For the producer

First the OrderTest.java that would look like this

@Test
public void validate_assertGetOrderById() throws Exception {
    // given:
        MockMvcRequestSpecification request = given()
                .header("Accept", "application/json");

    // when:
        ResponseOptions response = given().spec(request)
                .get("/orders/1");

    // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
    // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['id']").isEqualTo(1);
}

And the wireMock stub created from this contract. that looks like this:

{
  "id" : "a5367421-9668-4ea4-9663-40dc75d46278",
  "request" : {
    "url" : "/orders/1",
    "method" : "GET",
    "headers" : {
      "Accept" : {
        "matches" : "application/json.*"
      }
    }
  },
  "response" : {
    "status" : 200,
    "body" : "{\"id\":1}",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "a5367421-9668-4ea4-9663-40dc75d46278"
}

All you need to do is deliver a base class for the generated tests, that has the needed wiring. Our base looks like this:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderBase {

    @Autowired
    WebApplicationContext applicationContext;

    @Before
    public void setup() {
        RestAssuredMockMvc.webAppContextSetup(applicationContext);
    }
}

for the Consumer

As a consumer you want to integrate with a stubbed backend, you all-ready have the stubs.
You just need to add them to your pom and you can write tests like these:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ConsumerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableStubRunnerServer
public class ConsumerControllerIT {

    @Autowired
    private ConsumerController clientController;

    @Test
    public void indexPage() {
        String indexPage = clientController.index("1");
        Assert.assertTrue("response does not contain expected data", indexPage.contains("Your order data: Order{id='1'}"));
    }
}

As you can see you can validate your own consumer against the stubbed calls. This way both consumer and producer are integrating continuously.

If you need to go over it some more, here’s the link to the full code: https://github.com/sourcelabs-nl/spring-cloud-contract-demo

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *