- Published on
Getting Pact Tests Working on the Consumer Against a MockServer
- Authors
- Name
- Yair Mark
- @yairmark
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:
@PactVerification("test_provider")
: You need this to get Pact to fire off the MockServer for you- "test_provider": can be any name that makes sense to you but it needs to match with
.hasPactWith("test_provider")
in the Pact definition
- "test_provider": can be any name that makes sense to you but it needs to match with
ConsumerPactBuilder
: use this to build up your Pact/contract against the consumer you are testing.consumer("test_consumer")
: As with the provider name this is also just a name and can be called anything that makes sense to youuponReceiving("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)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
method("GET")
: This is the HTTP verb the consumer service is expecting for this requestwillRespondWith()
: simply indicates that you will now define what service response to expectstatus(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 validheaders(headers)
: this is a Java HashMap of key to value mappings of HTTP Header keys and values you are expecting in the service resultbody(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 usebodyWithSingleQuotes(
and replace all\"
with'
for readability.toPact()
: this builds the Pact using the values you provided in the builder lines above this one
MockProviderConfig config = MockProviderConfig.createDefault()
: I still need to dig more into what this can do but for now the default config seems to work finerunConsumerTest
: This is pretty important you need to ensure you import this as a static importimport static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest;
- 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
- 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. 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- 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.