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.
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:
We will share it with our provider to verify the contract.
The provider
Here’s the code to illustrate how the provider is implemented.
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.
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.
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.
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:
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. 🙂