Published on

Getting Pact Tests Working on the Consumer Against a MockServer

Authors

Yesterday I spoke a little about using Pact to test the communication between microservices. Today I will go a little more in depth into how to use Pact to test the consumer of a service. The difficulty I had was when looking at the GitHub page on this is that there were a number of ways of doing this but not one clear example of how to do this. It is not clear whether a solution needs some of or all the parts mentioned. Looking at other examples online it is also inconsistent as it sometimes seems like you need a Maven plugin (on the consumer side you do not need this) and the API they use has changed a bit since they wrote their post.

After a bit of fiddling I worked it out. First you need to use the correct Maven/Gradle dependency. As of the writing of this post I used the newest stable Maven consumer dependency (it is important when testing the consumer that you use the consumer dependency and not the producer one):

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit_2.12</artifactId>
    <version>3.5.14</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.12.0</version>
    <scope>test</scope>
</dependency>

In the above I had to exclude scala-library from the Pact dependency and include the expected version of Scala as I got the following error: NoSuchMethod scala.Product.$init$(Lscala/Product;)V when trying to run the test/Pact.

Now with these dependencies in place you can define a new JUnit test. Ensure it is named like any other JUnit test where the class name ends with Test otherwise the Pact Test will not run.

package your.company.integration.contract;

import au.com.dius.pact.consumer.ConsumerPactBuilder;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.PactVerificationResult;
import au.com.dius.pact.model.MockProviderConfig;
import au.com.dius.pact.model.RequestResponsePact;
import org.junit.Test;
import org.springframework.web.client.RestTemplate;

import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

public class WhateverNameMakesSenseConsumerPactTest {

    //this does not have to be test_provider it can match whatever you use in your Pact builder
    @Test
    @PactVerification("test_provider")
    public void name() {

        YourRestObjectYoureExpecting expectedObject = new YourRestObjectYoureExpecting(
            "John",
            "Smith",
            ...
        );


        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");

        RequestResponsePact pact = ConsumerPactBuilder
            .consumer("test_consumer") //this can be called whatever makes sense to your domain
            .hasPactWith("test_provider")//this can be called whatever makes sense to your domain
            .uponReceiving("A request for user information") //this is for information purposes
            .path("/your/api/path")
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(headers)
            .body(expectedObject.asJson()) //this asJson method is not a pact thing this object has a method defined on it to output the object as a JSON string
            .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault();
        PactVerificationResult result = runConsumerTest(pact, config, mockServer -> {
            RestTemplate restTemplate = new RestTemplate();

            YourRestObjectYoureExpecting actualObjectReturned = restTemplate.getForObject(mockServer.getUrl() + "/your/api/path", YourRestObjectYoureExpecting.class);

            assertThat(actualObjectReturned, is(equalTo(expectedObject)));


        });

        if (result instanceof PactVerificationResult.Error) {
            throw new RuntimeException(((PactVerificationResult.Error) result).getError());
        }

        assertEquals(PactVerificationResult.Ok.INSTANCE, result);
    }
}

The key things to watch out for here are:

  1. @PactVerification("test_provider"): You need this to get Pact to fire off the MockServer for you
    1. "test_provider": can be any name that makes sense to you but it needs to match with .hasPactWith("test_provider") in the Pact definition
  2. ConsumerPactBuilder: use this to build up your Pact/contract against the consumer you are testing
    1. .consumer("test_consumer"): As with the provider name this is also just a name and can be called anything that makes sense to you
    2. uponReceiving("description of test"): this simply holds the description of the Pact test. Like any test it should be something descriptive that will make sense if the test fails (i.e. it properly describes what the test is doing)
    3. path("/your/api/path"): Online many example have "/pact" which is confusing, this is actually the path of the API you are testing (after the host and port), it is not /pact
    4. method("GET"): This is the HTTP verb the consumer service is expecting for this request
    5. willRespondWith(): simply indicates that you will now define what service response to expect
    6. status(200): this is the HTTP status code you are expecting the service to respond with on a successful call - all Pact tests should be positive tests and never negative tests as you are checking the web service contract is valid
    7. headers(headers): this is a Java HashMap of key to value mappings of HTTP Header keys and values you are expecting in the service result
    8. body(expectedObject.asJson()): this takes the JSON string of what you are expecting in the body of the response. In my case I added a method to the DTO I was expecting back which used Jackson internally to write that object as a string. This .body method expects Strings inside it to use double quotes " which can get quite messy if you explicitly specify the String in here as you would have to escape all double quotes with \". But there is another .body* you can use if you need to specify the string by hand rather use bodyWithSingleQuotes( and replace all \" with ' for readability.
    9. toPact(): this builds the Pact using the values you provided in the builder lines above this one
  3. MockProviderConfig config = MockProviderConfig.createDefault(): I still need to dig more into what this can do but for now the default config seems to work fine
  4. runConsumerTest: This is pretty important you need to ensure you import this as a static import import static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest;
    1. This avoids you having to create a rule for the MockServer and in theory you should be able to have multiple Pacts in the same test class using different MockServers using this approach. If you write a test template class for this you can hide away much of this boiler plate and reuse this chunk easily
    2. In my case I use the Spring RestTemplate to make an HTTP call to the MockServer which Pact runs for us because of the @PactVerification annotation on top of the test. You can use any client you want to make an HTTP request.
    3. mockServer.getUrl(): will give you the base URL of the Pact MockServer you then still need to specify the path of the Pact you are testing, this must match what you specified .path( in the Pact definition defined previously
    4. You can then do standard JUnit asserts against the data returned

The last piece of the test seems to be fairly standard for Pact test and simply checks that the Pact response returned by the method (where we did our asserts in) is a valid Pact response and had no errors. The lines I am talking about from the above are:

if (result instanceof PactVerificationResult.Error) {
            throw new RuntimeException(((PactVerificationResult.Error) result).getError());
}

assertEquals(PactVerificationResult.Ok.INSTANCE, result);

When this test runs successfully a JSON file is generated under /test/pacts by default. The file will have the name test_consumer-test_provider.json where test-consumer is the value you put under consumer( and similarly test-provider is the value you put in hasPactWith( when you were building the Pact. You can override the location of where this file gets saved to but for simplicity I left it in the default location. In future posts I will get more into how Pact works on the producer side.