Consumer Driven Contracts with Pact (Part 1)

So this is not exactly a new topic. Consumer driven contracts have been around for years, as has Pact – a library that you can use to implement this testing approach. I have worked on projects before that used the approach and the library, even though it wasn’t a particular focus of mine. I had some time on my hands lately, so wanted to dive into the topic a bit, but when I tried to setup an example project with up-to-date dependencies, I got frustrated over the official documentation and other online resources that seem to only use older versions of Pact. That’s why I thought it might be a good idea to write up my learnings. If it won’t help you, it surely will help me remember how to work with Pact, or better PactJVM, next time I need it. I’m not claiming this is the best approach and there’s probably a lot to improve on, so feel free to share your experiences.

Consumer driven contracts

Modern applications need to be tested. There are different approaches and levels of testing that yo can apply. Many applications are built from multiple services and traditionally projects employ integration tests to cover use cases that span multiple service. While this works, the approach often requires a lot of setup, is hard to maintain and very brittle.

An alternative approach are Consumer Driven Contracts. These define dependencies between providers and consumers through contracts, which are created by customers and shared with providers, where they are automatically validated. Consumers work against a provider mock when defining a contract, producers use that contract to verify they comply with it. Consumer Driven Contracts don’t test application logic, they only enforce expectations on service interfaces.

As mentioned above, one framework to implement Consumer Driven Contracts is Pact. There are numerous language specific implementations, we will be using PactJVM here.

Project setup

My comfort zone is Spring Boot, so in order to get started I created a simple Spring app using the Spring Web Starter dependency. Using Pact requires us to add additional dependencies, but while the official documentation has many guides, it doesn’t do a good job to get you started – at least that’s my experience. With some iterations I arrived at a few working implementations. You can look at my code on Codeberg.

My sample project implements a very basic UserController, backed by a UserRepository that delivers some static user data. There’s also a User record to model the data I will be working with. To keep matters simple, I am implementing both consumer and provider test in on project. This takes away the burden of having to share the contracts between them. In a real world scenario you have to deal with it, possibly by running a pact broker. I won’t go into more details on that here.

Plain manual consumer and provider tests

The docs have different sections, the part that is most interesting to me is in the Consumer and Provider section. There are different sub-sections here, we’ll start with the Pact consumer and Pact provider menu items. Both pages reference out-of-date dependency version (4.2.x), though. We’ll use more recent versions. As of now, that’s version 4.4.2 for both consumer and provider dependencies. There are some differences in the code in contrast to what’s advised in the documentation, for that reasons.

testImplementation 'au.com.dius.pact:consumer:4.4.2'
testImplementation 'au.com.dius.pact:provider:4.4.2'

The consumer

Having added these two dependencies to the gradle build file we can implement our consumer an provider test code.

public class PactConsumerTest {
@Test
public void getAllUsersPact() {
String expectedUsers = """
[
{
"id": 1,
"name": "Jane Doe"
},
{
"id": 2,
"name": "John Doe"
}
]
""";
RequestResponsePact pact = ConsumerPactBuilder
.consumer("UserConsumer")
.hasPactWith("UserService")
.uponReceiving("A request for a user list")
.path("/users")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(expectedUsers)
.toPact();
MockProviderConfig config = MockProviderConfig.createDefault();
PactVerificationResult result = runConsumerTest(pact, config, PactConsumerTest::getAllUsers);
if (result instanceof PactVerificationResult.Error) {
throw new RuntimeException(((PactVerificationResult.Error) result).getError());
}
assertEquals(new PactVerificationResult.Ok(
Arrays.asList(
new User(1, "Jane Doe"),
new User(2, "John Doe")
)), result);
}
private static List<User> getAllUsers(MockServer mockServer, PactTestExecutionContext context) {
RestTemplate restTemplate = new RestTemplateBuilder().rootUri(mockServer.getUrl()).build();
User[] resultingUsers = restTemplate.getForObject("/users", User[].class);
if (resultingUsers != null) {
return Arrays.stream(resultingUsers).toList();
} else {
return Collections.emptyList();
}
}
}

As you can see, we setup the consumer pact using a builder. After that we run the test using the runConsumerTest() method. We pass several parameters to this method: The first on is the pact that we configured before. The second one is a mock provider configuration – we just use the default here. The last parameter is an implementation of the PactTestRun interface – it will execute a request against the mock server, that is started by Pact. We implement it through the method reference getAllUsers(). Inside, we pick up on the url from the mockserver, configure a Spring RestTemplate, send a request to that mock server and return the (configured) result.

The test is finished by some error handling and an assertion.

As mentioned above, the consumer test will create a contract for all the configured interactions between a consumer and a provider. This contract is serialized as a json file into the build directory of the project. Here, it is build/pacts and if you run it you will find a file called UserConsumer-UserService.json. It looks like this:

{
"consumer": {
"name": "UserConsumer"
},
"interactions": [
{
"description": "A request for a user list",
"request": {
"method": "GET",
"path": "/users"
},
"response": {
"body": [
{
"id": 1,
"name": "Jane Doe"
},
{
"id": 2,
"name": "John Doe"
}
],
"headers": {
"Content-Type": "application/json"
},
"status": 200
}
}
],
"metadata": {
"pact-jvm": {
"version": "4.4.2"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "UserService"
}
}

We will share it with our provider to verify the contract.

The provider

Here’s the code to illustrate how the provider is implemented.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PactProviderTest {
ProviderInfo providerInfo;
ConsumerInfo consumerInfo;
static Pact consumerPact;
@LocalServerPort
int port;
@BeforeEach
void setup() {
providerInfo = new ProviderInfo("UserService");
providerInfo.setProtocol("http");
providerInfo.setHost("localhost");
providerInfo.setPort(port);
providerInfo.setPath("/");
consumerInfo = new ConsumerInfo("UserClient");
consumerInfo.setName("consumer_client");
consumerInfo.setPactSource(new FileSource(new File("build/pacts/UserConsumer-UserService.json")));
//noinspection ConstantConditions
consumerPact = DefaultPactReader.INSTANCE.loadPact(consumerInfo.getPactSource());
}
@Test
void runConsumerPacts() {
// grab the first interaction from the pact with consumer
Interaction interaction = consumerPact.getInteractions().get(0);
// setup the verifier
ProviderVerifier verifier = setupVerifier(interaction, providerInfo, consumerInfo);
// setup any provider state
// setup the client and interaction to fire against the provider
ProviderClient client = new ProviderClient(providerInfo, new HttpClientFactory());
Map<String, Object> failures = new HashMap<>();
//noinspection ConstantConditions
VerificationResult result = verifier.verifyResponseFromProvider(
providerInfo,
interaction.asSynchronousRequestResponse(),
interaction.getDescription(),
failures,
client);
if (!(result instanceof VerificationResult.Ok)) {
verifier.displayFailures(List.of((VerificationResult.Failed) result));
}
assertThat(result).isInstanceOf(VerificationResult.Ok.class);
}
private ProviderVerifier setupVerifier(Interaction interaction, ProviderInfo provider, ConsumerInfo consumer) {
ProviderVerifier verifier = new ProviderVerifier();
verifier.initialiseReporters(provider);
if (!interaction.getProviderStates().isEmpty()) {
for (ProviderState providerState : interaction.getProviderStates()) {
//noinspection ConstantConditions
verifier.reportStateForInteraction(providerState.getName(), provider, consumer, true);
}
}
verifier.reportInteractionDescription(interaction);
return verifier;
}
}

As you can see, there’s a bit of setup involved. Firstly, we use the @SpringBootTest annotation to bootstrap our service. In the setup() method, we wire the service to the ProviderInfo container, most importantly we need to set the tcp port our service is running on.

Additionally, we need to reference the pact file that has been created by the consumer in the ConsumerInfo container. The names of the provider and consumer are important as they need to match in the consumer and provider tests.

There is one actual test method in the code, runConsumerPacts(). It reads from the pact, does some more setup for the verification and will then execute the interactions from the pact against our service. In the end it will display possible failures and assert on the result.

I don’t know how you feel about this approach, to me it’s a lot of technical boilerplate code. Fortunately we can improve on it, though.

JUnit5 style tests

Pact provides utility infrastructure for JUnit4, but in 2022 I don’t want to use JUnit4 unless I absolutely have to. So let’s look at what Pact has to offer for JUnit5, instead. You’ll see that our tests will be shorter and are structured in a better way. In order to avoid conflicts, I’m changing the provider name a bit so we work on a different contract file.

First, we need to add another dependency to our build file. Note that we use the latest version of the artifact as well, not the one from the offical docs.

testImplementation 'au.com.dius.pact.consumer:junit5:4.4.2'

The consumer

We use a new test class for our JUnit5 based consumer test.

@ExtendWith(PactConsumerTestExt.class)
public class PactConsumerJUnit5Test {
@Pact(provider = "UserServiceJUnit5", consumer = "UserConsumer")
public V4Pact getAllUsers(PactBuilder builder) {
String expectedUsers = """
[
{
"id": 1,
"name": "Jane Doe"
},
{
"id": 2,
"name": "John Doe"
}
]
""";
return builder
.usingLegacyDsl()
.given("A running user service")
.uponReceiving("A request for a user list")
.path("/users")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(expectedUsers)
.toPact()
.asV4Pact()
.get();
}
@Test
@PactTestFor(providerName = "UserServiceJUnit5", pactMethod = "getAllUsers")
void getAllUsers(MockServer mockServer) {
RestTemplate restTemplate = new RestTemplateBuilder().rootUri(mockServer.getUrl()).build();
User[] response = restTemplate.getForObject("/users", User[].class);
assertThat(response)
.isNotNull()
.containsExactly(
new User(1, "Jane Doe"),
new User(2, "John Doe")
);
}
}

First, we need to extend our test class with the Pact Consumer test extension. Then we need to define our pact in a method annotated with the @Pact annotation. Its parameters connect the pact to a provider and a consumer. The content of the pact is the same as before. There’s a difference to the code required by the older dependency version, though: the method signature changed as Pact now implements a newer version of pact contracts (that’s version 4). Since I have not dived into that, yet, I’m falling back to using the legacy DSL in the builder and then converting the pact to a version 4 packed in the end. It works, but I will need to follow up on that on. Also, note that we use the given() method, that can be used to setup initial state. We didn’t need to to that in the example above.

Finally we need a test method that will execute the defined pact. We use the @PactTestFor annotation to reference the pact we want to use in the test – this is done by method name. JUnit5 injects the mock server as a parameter. The test goes on to create a RestTemplate, wire it up to the mock server and execute a request. The pact is serialized to the build folder as a json file, same as in the previous approach.

The provider

Okay, let’s move on to the provider.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("UserServiceJUnit5")
@PactFolder("build/pacts")
public class PactProviderJUnit5Test {
@LocalServerPort
int port;
@BeforeEach
public void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
// see: https://docs.pact.io/getting_started/provider_states
@State("A running user service")
void setupUserService() {
// no state setup ATM
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}

We want top run the serialized contract against our service, so we need to start it first. We do that using the @SpringBootTest annotation, assigning a random port. The port is then injected into the port variable using the @LocalServerPort annotation. We use it in a setup method that configures the Pact verification context to connect to our service.
We also need the @Provider annotation and specify the base folder where our contracts reside using the @PactFolder annotation.

Next we need to map the state initialization from the consumer pact to a method annotated with @State. Currently we don’t do anything here, but it needs to be there.

As the test framework creates test runs for all the interactions in the serialized contract, we do not defined specific tests, instead we define a @TestTemplate that is extended with a PactVerificationInvocationContextProvider. Here, it doesn’t do much except to initiate the verification of an interaction.

Spring style testing

This is just a small variation of the provider test above. As you might know Spring provides us with sophisticated testing infrastructure. Pact enables us to use it and this might be useful in some cases.

@WebMvcTest
@Provider("UserServiceJUnit5")
@PactFolder("build/pacts")
public class PactProviderSpringJUnit5Test {
@TestConfiguration
static class Config {
@Bean
public UserRepository userRepo() {
return new UserRepository();
}
}
@Autowired
MockMvc mockMvc;
@Autowired
UserRepository userRepository;
@BeforeEach
public void setup(PactVerificationContext context) {
context.setTarget(new MockMvcTestTarget(mockMvc));
}
// see: https://docs.pact.io/getting_started/provider_states
@State("A running user service")
void setupUserService() {
// no state setup ATM
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}

In contrast to the provider test above, we use the @WebMvcTest annotation. This enables us to use the mockMvc object, which we pass on to the MockMvcTestTarget in the setup method. As a @WebMvcTest only creates controller beans, but no repositories, we need to provide a repository to be used for injection in the test. We could setup a @MockBean but I just manually create a repository instance as it’s a dumb sample class, anyway.

Verifying contracts in your build tool

You don’t need to write the provider code at all. You can use a gradle plugin or a maven plugin to do that. If you don’t need to to something special in your provider tests, then this might be a convenient way to verify your contracts. You will then use either

gradle pactVerify

or

mvn pact:verify

to verify your contracts. But that also means you need to provide a running service that the provider tests can run against. I did not dive into this any deeper, at this time.

Matching responses

The examples above use static data for the request responses. That’s not very practical. In reality, you want to define a much looser contract, where you might define response properties, but no values. You can do that with Pact. Here’s an example with an updated pact definition method:

@Pact(provider = "UserServiceJUnit5", consumer = "UserConsumer")
public V4Pact getAllUsers(PactBuilder builder) {
return builder
.usingLegacyDsl()
.given("A running user service")
.uponReceiving("A request for a user list")
.path("/users")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonArray()
.object()
.integerMatching("id", "[0-9]*", 1)
.stringMatcher("name", "[a-zA-Z ]*", "Jane Doe")
.closeObject()
.object()
.integerMatching("id", "[0-9]*", 2)
.stringMatcher("name", "[a-zA-Z ]*", "John Doe")
.closeObject()
)
.toPact()
.asV4Pact()
.get();
}

The PactDslJsonArray class provides us with a convenient DSL. We can use it to build up the expected content and use regular expressions for expected values. We can also provide example values. The resulting contract will contain a new section named matchingRules, which represents what we define here. The provider can then use it to match the actual service response against. There might be more interesting stuff here, in case you want to read up on it.

What else?

My sample project doesn’t go into details about how to share contracts between consumers and providers. In a real world use case you will have to answer that question, though. An obvious way to go is to run a Pact broker, but you can come up with manual solutions, as well. I hope this post helps you find your way into Pact, though. It’s not complete and there might be other and better ways to use it, but at least it is not completely outdated. Not at this point in time, at least. 🙂