Consumer-Driven Contract Testing with Spring Cloud Contract

Tommy Situ,

Testing microservices becomes a daunting task as the system topology grows. When microservices are chained together to realise business functionality, it is tempting to verify that they are working together by writing integration tests. If you go down this path, you will need to have all your applications, the underlying resources (such as databases, S3 buckets), and third party APIs wired up and running in a known state just to ensure that “service A” can talk to “service B”.

The fact that this is cumbersome to set up is not the only issue here. Your tests might sometimes fail mysteriously as well. “Sometimes” can mean all sorts of flakiness, like network timeouts, third party API rate limiting, or simply data left behind from the previous test run. Managing all these moving parts is too much of a pain if you just want to test the API of one microservice.

Luckily, mocking dependent services is possible using service virtualization tools like Hoverfly or WireMock. Testing the integration between services A and B becomes an isolated component test for service A, with an embedded stub of service B.

However, this creates another dilemma: how do you guarantee that service B’s stub always tracks the changes of the actual service? Imagine the developer working on service B quietly rolls out an API update which invalidates its stub used by service A, and the continuous deployment pipeline gives a green light for release based on service A’s passing tests. This would eventually lead to firefighting in production.

Perhaps it is time to think about having an agreement between the two services. Service A (as a consumer) creates a contract that service B (as a producer) will have to abide by. This contract acts as the invisible glue between services - even though they live in separate code bases and run on different JVMs. Breaking changes can be detected immediately during build time.

This is known as Consumer-Driven Contract (CDC) testing, an effective way of testing in a distributed architecture that complements service virtualization. In this blog, I will introduce Spring Cloud Contract: a CDC framework for JVM-based projects, especially those using Spring Boot.

A simple use case

In this demo, we have two microservices: subscription and account. We need to add a new feature to the subscription service so that a subscription for a friend’s account will be free. To find out if an account is labelled as ‘friend’, the subscription service needs to consume the account service’s “get account by ID” API. You can find the source code for this blog on GitHub.

Consumer-Driven Contract Testing

What you need

  • Java
  • Spring Boot (1.4.1.RELEASE)
  • Spring Cloud Contract (1.0.1.RELEASE)
  • Gradle (3.1)
  • Maven repository

A sample Gradle build file can be found on the Spring Cloud Contract project site.

The key dependencies are spring-cloud-starter-contract-verifier for the producer to auto-generate API verification test, and spring-cloud-starter-stub-runner for the consumer to auto configure a stub server.

Step-by-step workflow

CDC testing is analogous to TDD at the architecture/API level, and therefore shares a similar workflow.

Add a test: On the consumer side, we start by writing the functional test for the new feature, and implementing the gateway that communicates with the producer endpoint.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SubscriptionTest {
   @Autowired
   private SubscriptionService service;

   @Test
   public void shouldGiveFreeSubscriptionForFriends() throws Exception {

       // given:
       String accountId = "12345";
       Subscription subscription = new Subscription(accountId, MONTHLY);

       // when:
       Invoice invoice = service.createInvoice(subscription);

       // then:
       assertThat(invoice.getPaymentDue()).isEqualTo(0);
       assertThat(invoice.getClientEmail()).isNotEmpty();
   }
}

Run all tests: Obviously they fail

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/account/12345": Connection refused.

Write some code: The missing implementation is no longer in the same code base. We need to check out the producer’s repository, and add a contract using the Spring Cloud Contract Groovy DSL based on how the consumer expects the producer to behave. This file should be located in src/test/resources/contracts/ for the spring-cloud-contract-gradle-plugin to find.

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

Contract.make {
   request {
       method 'GET'
       url value(consumer(regex('/account/[0-9]{5}')), producer('/account/12345'))
   }
   response {
       status 200
        body([
                type: 'friends',
                email: 'tom@api.io'
        ])
       headers {
           header('Content-Type': value(
                   producer(regex('application/json.*')),
                   consumer('application/json')
           ))
       }
   }
}

The contract consists of a request and response pair. It shows an example of using a dynamic value for URL path. With the value(consumer(…), producer(…)) helper method, it is possible to set matcher or concrete values. In this case, adding a regular expression on the consumer side (in the generated stub) to match on requests with any account ID, and setting a specific account ID for the generated test to match on the known state of the producer.

Once again, the producer side follows some sort of TDD pattern.

  1. Run gradle generateContractTests to generate the test in the build folder:
public class ContractVerifierTest extends ContractVerifierBase {

  @Test
  public void validate_shouldReturnFriendsAccount() throws Exception {
     // given:
        MockMvcRequestSpecification request = given();

     // when:
        ResponseOptions response = given().spec(request)
              .get("/account/12345");

     // 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("type").isEqualTo("friends");
        assertThatJson(parsedJson).field("email").isEqualTo("[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>");
  }
}

The generated test relies on RestAssuredMockMvc to perform HTTP requests. To make it runnable, we also implement a base class that bootstraps the test environment, mocking out dependencies if necessary.

  1. On the producer side, we implement the ContractVerifierBase class to load web context and set up RestAssuredMockMvc
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AccountServiceApplication.class)
public class ContractVerifierBase {

   @Autowired
   private WebApplicationContext context;

   @Before
   public void setUp() throws Exception {
       RestAssuredMockMvc.webAppContextSetup(context);
   }
}

We also need the following settings in the build.gradle file to tell the spring-cloud-contractplugin to locate the ContractVerifierBase class:

contracts {
    packageWithBaseClasses = 'com.demo.account.contracts'
}
  1. Now we can implement the producer’s new endpoint to pass the test.
@RequestMapping(method = RequestMethod.GET, value = "/account/{id}")
public Account getAccount(@PathVariable String id) {
   return accountService.getById(id);
}
  1. After passing the contract verifier test, we have a satisfying contract! Running gradle clean build install will generate and publish the WireMock mappings as a stubs.jar file to the local maven repository. You could inspect the WireMock mapping file in the build/mappings folder:
{
 "uuid" : "79ab1fad-984f-4a6c-8b24-88deeb8cb503",
 "request" : {
   "urlPattern" : "/account/[0-9]{5}",
   "method" : "GET"
 },
 "response" : {
   "status" : 200,
   "body" : "{\"type\":\"friends\",\"email\":\"[[email protected]](/cdn-cgi/l/email-protection)<script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */</script>\"}",
   "headers" : {
     "Content-Type" : "application/json"
   }
 }
}

Run tests again: Finally, on the consumer side, we simply add

@AutoConfigureStubRunner(ids = "com.demo:account-service:+:stubs:8082", workOffline = true)

to the test that requires a producer stub. This stub runner will pull and unpackage the latest stubs jar file (as we set the version to a “+” symbol), start up a WireMock server on port 8082 and register the stub mapping.

Now we have the producer stub running, the test should pass.

Working in CI/CD environment

So far we have only looked at how to develop a new feature with CDC on a local machine. Integrating with the package/build pipeline requires a bit more tweaking:

  • The producer’s Gradle build task will generate and run contract verifier tests by default. It just needs to publish the stub jar to the remote repository by adding uploadArchivesto its Gradle tasks.
  • The consumer needs to configure StubRunner to resolve the stub. This can be done by setting the Spring Boot application properties:
stubrunner:
    ids: com.demo:account-service:+:stubs:8082
    repositoryRoot: https://demo.jfrog.io/demo/libs-snapshot</pre>

Conclusion

Consumer-Driven Contracts (CDC) give us fast feedback to verify the integration between microservices, and more confidence when deploying them independently without the fear of introducing breaking changes to other services.

Spring Cloud Contract provides a simple workflow for CDC testing with minimal coding. The advantage is that you can write a contract with the statically-typed Groovy DSL to generate producer verification tests and stub mapping files automatically. The downside is that crafting a contract file manually could be a curse in some cases. For instance, service interactions might have complex payloads or request bodies, and it would take a huge amount of effort to get it right.

There are also a few caveats:

  • Your CI environment should have integration with a maven repository to share stub jar files. Resolving stubs from a password-protected repository is not yet supported by Spring Cloud Contract at the time of writing.
  • Support for JVM-based project only. If you are looking for a CDC framework for Javascript, Go, .Net etc, Pact framework is a better option.
  • As an emerging project, you will expect to see some teething problems.

If you are interested in the source code for this demo, it can be found on GitHub.