Testing your contracts (3/5)

<i>Est Reading Time: </i> 11 minutes<br><br>

We’ve already looked at contract testing and the PACT framework and in part 3 we’ll be looking at using that framework to create your consumer side contracts.

Language Support

Pact supports many different languages and you can find details and guides for the supported ones at https://docs.pact.io/implementation_guides. I’ve used Pact in Javascript, Java and C# and have chosen to use Java for the examples here as it’s the one I’ve used most recently. The principles are the same, no matter which language you use though.

Scenario

So let’s look at the scenario we will use for this guide:

Consumer

Our consumer here will be a simple API that orchestrates data from a number of services. In this case it will be consuming from the Employee Service.

Provider

The provider will be the Employee Service is a REST service that covers CRUD operations for the Employee object. CRUD is Create/Read/Update/Delete in case it is not a term you have seen before.

Pact

We are going to use the following scenario for our pact:
Given An employee exists with id of 1
When I request to view that employee
Then I am returned success and a single employee object

Consumer Code

So let’s delve in to the code we need for the consumer side.

Dependancies

The first thing we need is to install the pact framework. With Java, this is a matter of adding the following package for the consumer code:

<dependency>
     <groupId>au.com.dius</groupId>
     <artifactId>pact-jvm-consumer-junit_2.11</artifactId>
     <version>3.5.23</version>
     <scope>test</scope>
</dependency>

The version is not too important – I’d generally get the latest stable and stick to that unless new features come up you wish to take advantage of.

Initial Class

So, let’s create the boilerplate code. Firstly we’ll create a class and call in EmployeeApiContractTest – it’s good to have a naming convention up front.  We’re then going to create two methods, one will be the method that sets up a mock and the request details and expected response and the other will be the one that hits this mock to generate the JSON pact file.

Whatever language you choose, this is the same pattern you will need – something to set the mock up and something to hit that mock and generate the pact file. This pattern is the supported pattern by the framework and in Java is accomplished by the annotations “@Pact” and “@PactVerification”.

Therefore we end up with the following class:

public class EmployeeApiContractTest { 

    private RestTemplate restTemplate = new RestTemplate();

    private final String PROVIDER_NAME = "employee-service";
    private final String CONSUMER_NAME = "public-api";

    @Rule
    public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2(PROVIDER_NAME, PactSpecVersion.V3, this);

    @Pact(provider = PROVIDER_NAME, consumer = CONSUMER_NAME)
    public RequestResponsePact shouldGeneratePactWhenRetrievingEmployeeWithIdOfOne(PactDslWithProvider builder) {
    }

    @Test
    @PactVerification(fragment = "shouldGeneratePactWhenRetrievingEmployeeWithIdOfOne")
    public void shouldReturnValidResponseWhenRetrievingEmployeeWithIdOfOne() throws URISyntaxException {
    }
}

Pact Mock Setup

Now we need to set up the mock. To do this, we need to set up the request we are going to send, where we are going to send it and the response that we expect back from the provider. We are also going to set a state for the provider so it knows what data needs to exist when we send it this request.

Each supported language has different ways of setting up this information but pact has unless documentation for each.

For Java, you can use a “domain specific language (DSL)” class called “PactDslJsonBody” to create your request and response.

Request

For this particular request, we have no JSON body we are sending in. We will be using the id of the employee we wish to get in the path so we won’t need to use the DSL to create a JSON body for the request. We’ll set the path we are hitting later on, for now we can go straight on to the response.

Response

For the response, will be expecting a JSON body with the following:

  • A root employee object, containing
    • An integer id
    • A string first name
    • A string surname

We can use the DSL to create this as follows:

PactDslJsonBody bodyResponse = new PactDslJsonBody() 
        .object("employee")
        .integerType("id")
        .stringType("firstName")
        .stringType("surname")
        .closeObject()
        .asBody();

As you can see the DSL describes the JSON as if you were writing it line by line. This is a loose description, which means that if there are extra fields we have not specified, these will not break the pact. This is what we want as we have specific the fields in the body we are using, if there is other information it’s not for us to care about. It may matter to a different consumer, but not us.

Another point here is that we do not verify the data in the fields. That’s because it’s not our responsibility to ensure the provider is working correctly, we just need to ensure we are getting the structure of the response we expect or we can’t extract the required data from the response. Therefore we use the “…type” methods, which just check the type of the field and the name of the field. The data could be anything, it’s not going to affect how we use it.

Mock

Now we have our response object which we are expecting, we need to set up our actual mock. This is where we say which endpoint we will hit and the expectations we have when we do that. This is the information that then forms the generated pact JSON:

return builder.given("An employee exists with id of 1") 
        .uponReceiving("a request to retrieve the employee")
        .path("/employee/1")
        .body(bodyRequest)
        .method(RequestMethod.POST.name())
        .willRespondWith()
        .headers(headers)
        .status(200)
        .body(bodyResponse)
        .toPact();

As you can see the PACT framework is written in a very readable language so most lines here are pretty self explanatory. The one I want to highlight is the “given”. This is where we describe the state and it is the link with the provider. As we will see in part 4, the provider needs to know this state and understand what it means. I’ll go deeper in to this in part 4, but it’s important to understand that it signifies any state that the provider needs to be in to satisfy this contract.

Pact Verification

Next we need to write the verification code. This is a piece of code which runs like a test – it will hit the mock with the given response and the pact framework the cause a JSON pact to be generated. The “verification” part of this is that it is effectively verifying the code that hits the mock endpoint. I’ll explain a bit more later on but for now, let’s write the verification code.

@Test
@PactVerification
public void shouldReturnValidResponseWhenRetrievingEmployeeWithIdOfOne() throws URISyntaxException {
    Integer employeeId = 1;
    EmployeeProvider employeeProvider = new EmployeeProvider(restTemplate);


    final ResponseEntity<GetEmployeeResponse> response = employeeProvider
            .getEmployeeById(new URI(mockProvider.getUrl()), employeeId);


    assertEquals(200, response.getStatusCodeValue());
}

Let’s example some aspects of this fairly simple method. The @Test annotation ensures that this test is run along with the unit tests and the @PactVerification annotation is part of the pact framework which ensures that the Pact method we have already written is invoked when we hit the relevant endpoint and the pact file is generated.

Encapsulated Code

An important part of the method above is the EmployeeProvider class. This class is a piece of code which encapsulates the code that calls the actual endpoint we are writing the contract for. This code can then be called by both the pact and the service code. This is very important because if we change the way we call the endpoint, we would want our pacts to fail. However if we don’t use a piece of shared code for the call, this would not happen unless we change the pact too – which we wouldn’t know to do. For context, this is the code within the EmployeeProvider:

@Component
@AllArgsConstructor
public class EmployeeProvider {

    private static final String GET_ENDPOINT = "/employee/";

    private final RestTemplate restTemplate;

    public ResponseEntity<GetEmployeeResponse> getEmployeeById(final URI urlPrefix, Integer id) {
        return restTemplate.getForEntity(
                urlPrefix + GET_ENDPOINT + id, GetEmployeeResponse.class
        );
    }
}

Asserting the response

Given the point of this test method is to generate the pact which is verified why are we actually performing any assertions on the result? There are two main reasons for this.

Verifying the mock

The first is that we want to ensure we have coded to hit the mock correctly and the easiest way for this test to ensure that is check we have a successful response. If we have, we can be fairly sure we’ve hit the mock and the pact will have been generated.

Unit testing

The second and biggest reason for performing the assertions is that in doing so, we are performing a unit test on the encapsulated code. We are not looking at these tests in place of unit tests on that code as that is not the responsibility of these tests and we’d still want to write quality unit tests for it. However we want to ensure here that if we change the encapsulated code for any reason that our pact test verification also fails. This will ensure that we investigate and change the pact where needed to match the changes we have made if that’s the chosen route. Without any assertion, we could not be aware of a failure and the pact test could stay as is – meaning we’d have a pact file that doesn’t actually represent the contract anymore!

Running the test

With Java, this is nice and simple because the @Test annotation tells JUnit to run the test as it would any other unit test! For the specific test runner you are using, you just need to treat it like a normal test and it will run as a normal unit test and fail if the assertions fail.

Output

The default for Java is to output the pacts to the target folder in a folder called “pacts”. This will change depending on the language (i.e. C# defaults to the bin folder) and can be configured to be a specific folder if needed. Pact has excellent documentation and examples for every supported language.

So, let’s see the output for this pact! Every field should make sense given the code we have written so far, so we won’t go through it in too much details but a few things to note specifically are:

  • The matchingRules in the response are the matchers we used earlier telling the provider to match on type and not match the data
  • The generators are what pact uses when hitting the mock in the consumer – because we don’t provide data for the mocked response, it has to generate some that meet the matching rules
  • The provider state is clearly noted on its own and is the most important part as the provider can’t verify the pact without knowing and coding for this state
{
    "provider": {
        "name": "employee-service"
    },
    "consumer": {
        "name": "public-api"
    },
    "interactions": [
        {
            "description": "a request to retrieve the employee",
            "request": {
                "method": "GET",
                "path": "/employee/1"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json;charset=UTF-8"
                },
                "body": {
                    "employee": {
                        "firstName": "string",
                        "surname": "string",
                        "id": 100
                    }
                },
                "matchingRules": {
                    "body": {
                        "$.employee.id": {
                            "matchers": [
                                {
                                    "match": "integer"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.employee.firstName": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.employee.surname": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        }
                    }
                },
                "generators": {
                    "body": {
                        "$.employee.id": {
                            "type": "RandomInt",
                            "min": 0,
                            "max": 2147483647
                        },
                        "$.employee.firstName": {
                            "type": "RandomString",
                            "size": 20
                        },
                        "$.employee.surname": {
                            "type": "RandomString",
                            "size": 20
                        }
                    }
                }
            },
            "providerStates": [
                {
                    "name": "An employee exists with id of 1"
                }
            ]
        }
    ],
    "metadata": {
        "pactSpecification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.23"
        }
    }
}

Summary

We’ve seen how you can write a simple consumer driven pact file using Java and discussed some of the patterns to look out for when we are writing it.

What do we do with this pact file now? We need to give it to the provider so they can verify it. In part 4, we’ll look at the provider side of this pact and creating the code to perform this verification.

Share this post:

Leave any thoughts below!