Consumer Driven Contracts – Test What You Can’t See

In the last article, the general concept of microservices as just another type of modularization was introduced. Benefits as well as disadvantages were shown.

One of these disadvantages is the higher effort of testing. For a monolithic application, integration tests are way easier to implement than for a number of distributed microservices. Another issue can be that dependents of one service are not always known or at least the specific requirements onto the used interface. Just think about an environment with >100 services, each of them individually developed with different teams involved. This environment results in the inability to freely advance the service because changes might break things on the other end.

Consumer-Driven Contracts help to mitigate this issue by letting the clients of your service formally define the usage, i.e. the contract of your API. this contract can than be verified during continuous integration, giving you safety to stay compatible to all dependents while being able to change things. There are basically two main approaches to this. During the test-phase of the consumer, a second artifact is created, containing explicit tests (e.g. JUnit-tests) that verify the provider-api. These tests are later used on the provider-side for verification. Another approach is to formally specify the contract in a format which is later interpreted on provider-side for verification.

In this article, I want to show how to implement Consumer-Driven Contracts from end-to-end, i.e. contract-specification, contract-verification and contract-distribution using Pact framework. It’s a framework for implementing consumer driven contract specification and verification based on contracts defined in pact-files. There are versions for Ruby as well as for the JVM. It allows to easily define Contracts (so-called ‘Pacts’) during the testing phase of the consumer. While these tests are running, a separate server is started that serves the specified responses upon receiving matching request and recording everything into the pact-file. After the tests are run, the whole contract is contained in this file. The pact-file can than be used on the producer-side later on to replay these requests and verify the responses, assuring that the contract still holds. The following picture shows an overview of the process:

Pact Verification Process
Pact Verification Process (source)

A good indepth explanation of the ruby-specific implementation can be viewed here.

As we at Affinitas mainly use Java, we are going to use the JVM-based implementation of Pact. An example of how to specify a contract in a JUnit test on consumer-side would look as follows.

public class ExampleJavaConsumerPactTest  {
    @Rule
    public PactRule rule = new PactRule("localhost", 8080, this);

    @Pact(state="test state", provider="test_provider", consumer="test_consumer")
    public PactFragment createFragment(PactDslWithState builder) {
        return builder
            .uponReceiving("ExampleJavaConsumerPactRuleTest test interaction")
                .path("/")
                .method("GET")
            .willRespondWith()
                .status(200)
                .body("{\"responsetest\": true}")
            .toFragment();
    }

    @Test
    @PactVerification("test state")
    public void runTest() {
        Map expectedResponse = new HashMap();
        expectedResponse.put("responsetest", true);
        assertEquals(new ConsumerClient("http://localhost:8080").get("/"), expectedResponse);
    }
}

First, a PactFragment needs to be created which defines the contract from consumer-side. This will then be used as mock-server and returnes the specified response during the execution of the `runTest`-method.

All that is left is a mean to transfer these files from a consumer to a provider in an asynchronous fashion, so that the provider can then be tested against this contract. It would be also nice to dynamically distribute these contracts so that the provider does not need to know beforehand which services are consumers. This can be done using the pact-broker or using git-repositories (the up- and download usign git-repositories can be integrated into the build-process with the pactbroker-maven-plugin).

The final pact-verification itself on provider-side can be done using the corresponding pact-jvm-provider-maven.

Consumer-Driven Contracts allow to decouple testing-dependencies and still achieve deep integration-testing. It allows to develop and advance microservices with a safety-net and without the hassle to have to spin up all services at once just for integration-testing.

Leave a Comment

Your email address will not be published.