This is a 10-step tutorial to help learn how to do consumer-driven contract testing using the Pact tool. Each commit in this tutorial progressively explains a particular aspect of how this is intended to work. As part of the tutorial we are building a very simple client (Kotlin) which makes requests GET requests to a customer details service (Java, Spring Boot, R2DBC) which is expected to return first and the last names of a customer given an identifier. For example, an HTTP request like below:
GET --header "Accept: application/json" /customers/12345is expected to produce output like below:
{
"id" : 12345,
"firstName" : "Test",
"lastName" : "First"
}Consumer-driven contract testing requires close collaboration between the consumer and the provider teams. The use of tools like Pact facilitates this collaboration in a positive way. A proposed high level flow when working with Pact (and the approach used in this tutorial) is shown here:

To run the code examples from this tutorial, you will need:
- JDK 13 installed
- Kotlin 1.3.x installed
- Docker 19.x installed
- Optionally, an IDE such as IntelliJ
- Run the Pact Broker: The easiest way to do this locally is to checkout the dockerized pact broker and run
docker compose upfrom the command line. If all goes well, you should be able to navigate to http://localhost/ and see the pact broker screen as shown below:
This tutorial is organized in 10 discrete steps. You can choose to checkout and start at any one of these steps depending on the amount of work you want to do yourself. The actual steps in the tutorial itself are outlined below:
| Step Number | Description | Start here if you want to |
|---|---|---|
| Step 01 | Client project setup | Do everything on your own from scratch |
| Step 02 | First contract test | Understand how to write a pact and an accompanying consumer driven contract test |
| Step 03 | Sharing the pact with others | Understand how to setup your project to be able to share pacts with others |
| Step 04 | Provider implementing the API | Have a starting point with the provider having implemented the API |
| Step 05 | Provider honoring the contract | Understand how the provider can pull pacts and honor the contract |
| Step 06 | Provider publishing the contract verification | Understand the setup required to enable the provider to publish pact verification results |
| Step 07 | Reusing pact for other tests | Understand how pacts can be reused to serve as stubs in other testing contexts |
| Step 08 | Writing less fragile contract tests | Understand how to refactor pacts to rely less on hard-coded values |
| Step 09 | Added a second pact for a non-existent customer | Understand how to write pacts to cover additional interactions |
| Step 10 | Accommodating for two styles of provider verification | Understand how you can reduce the amount of integration setup on the provider side |
Note: You can checkout the respective step to start from that point or simply compare the evolution of the project between commits.
The significant portions of each step are described in detail below:
This step shows you how to set up a Kotlin project for use with JUnit5 andPact with the Gradle build tool.
To introduce JUnit5 support, add the following to build.gradle.kts dependencies
testImplementation("org.junit.jupiter:junit-jupiter:5.6.0")To prompt gradle to start using JUnit5, add the following:
tasks.withType<Test> {
useJUnitPlatform()
}To introduce pact support, add the following to build.gradle.kts dependencies
testImplementation("au.com.dius:pact-jvm-consumer-junit5:4.0.6")There are two steps to doing this. The team consuming the service, starts by writing a pact, which is essentially an expression of the expectations from the service provider. When working with JUnit5, you need to create a test class and annotate it like below:
@ExtendWith(PactConsumerTestExt::class)
class CustomerServiceContractTests {
//...
}You then write a method annotated with the @Pact annotation. A sample pact is shown below:
@Pact(provider = "CustomerService", consumer = "AndroidClient")
fun getDetailsById(builder: PactDslWithProvider): RequestResponsePact {
return builder.given("A customer with an existing ID")
.uponReceiving("a request for customer details")
.path("/customers/1234")
.headers(mapOf("Accept" to "application/json"))
.willRespondWith()
.headers(mapOf("Content-Type" to "application/json"))
.body(
PactDslJsonBody()
.stringType("firstName", "Test")
.stringType("lastName", "First")
)
.status(200)
.toPact()
}A few notable things in the code above:
- As part of the
@Pactannotation, you need to specify values for theproviderand theconsumerattributes. Theconsumeris an agreed upon convention to represent your current codebase, and similarly, theproviderrepresents the service provider. It is advisable to use the same identifiers for all pacts when referencing the same consumer and producer respectively. - The
PactConsumerTestExtextension injects aPactDslWithProviderinstance into thegetDetailsForExistingCustomerIdmethod, which provides you with the ability to describe the structure of the request to be made to the service and the response that can be expected back. - The
givenmethod on thebuilderaccepts a single parameter which is called the provider state. The provider state can be thought of as the identifier for your scenario (called an interaction in Pact parlance) and can be referenced in other contexts (for e.g. to setup state on the provider prior to verifying the contract). - The
bodymethod can be used to describe the structure of either the request (applicable for HTTP methods that require one) or the response body. Pact supports a number of ways to do this. ThePactDslJsonBodyclass provides a way to construct a response. To examine other ways in which the response can be created
This is then followed by writing a unit test which asserts the expectations expressed in the pact above. A corresponding sample is shown below:
@PactTestFor(pactMethod = "getDetailsForExistingCustomerId")
@Test
fun testForGetDetailsByIdForExistingCustomer(mockServer: MockServer) {
//...
}The pactMethod attribute of the PactTestFor annotation needs to match the name of the method annotated with @Pact exactly (otherwise this will result in a test failure). The PactConsumerTestExt extension injects a MockServer instance, which can be used to make HTTP requests and verify the expectations expressed in the pact.
Note: It is important to note that this is a unit test only. At this stage, there is no correlation (yet) with the provider. However, it does provide a quick and cheap means to validate the expectations expressed in the pact to yourself and your own team.
Now that you have written your first pact and the accompanying (and passing) unit test, it is time to share this with others (most pertinently with the provider team for verification). When you run your unit tests, Pact creates a [pact file], which describes the interaction(s) codified in your tests in a platform neutral manner. There are a variety of ways to share the pact file(s). We will use the Pact Broker, a repository (similar to other artifact repositories), purpose built to share pacts, to do this. Assuming that the pact broker is running, you can do this by doing the following in the build.gradle.kts file:
- Introduce the pact plugin
plugins {
id("au.com.dius.pact") version "4.0.6"
}- Configure the
pactPublishtask
pact {
publish {
pactDirectory = "${project.buildDir}/pacts"
pactBrokerUrl = "http://localhost"
}
}- Finally, invoke the task to publish local pacts to the pact broker
./gradlew clean build pactPublishAssuming that the pact broker is reachable and running, you should see a message similar to:
> Task :pactPublish
Publishing 'AndroidClient-CustomerService.json' ... HTTP/1.1 201 Created
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executedIf you navigate to the Pact Broker itself, you should see something like below:

The provider in this example is a very simple Spring Boot application which uses webflux and r2dbc for web API and data access respectively. Look at the CustomerController and the CustomerControllerTests for details on the implementation of the GET customer by id API.
To honor the contract with the client, the provider needs to first pull the pacts (in this case, from the Pact Broker) and then verify each one. To do this, the provider needs to write a test which looks like below:
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Provider("CustomerService")
@PactBroker(host = "localhost", port = "80")
public class AndroidClientIntegrationContractTests {
@Autowired
private CustomerRepository repository;
@LocalServerPort
private Integer port;
@State("an existing customer with a valid id")
void pactWithAnExistingCustomer() {
Flux.just(1234L)
.map(id -> new Customer(id, "Test", "First"))
.flatMap(repository::save)
.blockLast();
}
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}A few notable things in the code above:
- The
@ExtendWithannotation is standard fare for JUnit5 tests, whileSpringExtentionand@SpringBootTestare standard spring boot incantations. It is important to note that this is an integration style test because this does require that the web server be running (on a random port in this case). - The
@Providerannotation is part of pact and signifies that we are working with theCustomerServiceprovider. Note that this has to match exactly with the value of theproviderattribute chosen when writing the pact on the consumer side. - The
@PactBrokerannotation specifies the location of a running Pact Broker which houses all the pacts for this provider. If this is a non-starter, there are other options. Pact simply needs to locate the pact file(s) and allows you to specify the source of your pacts (using the@PactSourceannotation). This allows loading pact files using other built-inPactLoaderimplementations or writing your own custom ones. - The
@Stateannotation helps setup (or teardown) for specific provider states. In this example, we only insert data into the database as part of setup, but don't need any teardown because we are using an in-memory database.
To publish verification results back to the Pact Broker, you need to set two system properties pact.provider.version and pact.verifier.publishResults to the provider's current artifact version and true respectively. This can be done when running tests as part of the build. With gradle, this will mean doing something like:
tasks.withType<Test> {
systemProperty("pact.provider.version", version)
systemProperty("pact.verifier.publishResults", true)
}Or with Maven, something like:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<pact.provider.version>${project.version}</pact.provider.version>
<pact.verifier.publishResults>true</pact.verifier.publishResults>
</systemPropertyVariables>
</configuration>
</plugin>When verification results are published to the broker, this should appear something like below on the pact broker console:

Note how the pact interaction captures the exact and respective versions of both the consumer and the provider. This makes it very objective when making deployment decisions to assess compatibility of these components. For more details on how this works, look at the "can I deploy" page from the Pact documentation.
Pact file(s) have enough information to recreate stub scenarios. It will be really nice if we could use these stubs elsewhere in other tests as well (for e.g. to act as a data provider for more expensive UI-based functional tests). The pact-stub-server allows us to do just that. Conveniently, this is published as a docker image. Combining this with the excellent testcontainers library allows us to make this a part of other kinds of tests as well. An example written in Kotlin is shown below:
@Testcontainers
class CustomerServiceFunctionalTests {
private val logger = LoggerFactory.getLogger(CustomerServiceFunctionalTests::class.java)
private val pactBrokerAddress = "172.17.0.1" // IP address of host from the docker container
@Container
private val stubContainer: GenericContainer<Nothing> = GenericContainer<Nothing>("pactfoundation/pact-stub-server")
.apply {
withExposedPorts(8080)
withCommand("-u", "http://${pactBrokerAddress}/pacts/provider/CustomerService/consumer/AndroidClient/latest",
"-p", "8080")
withLogConsumer(Slf4jLogConsumer(logger))
}
@Test
fun shouldRunFunctionalTest() {
val host = stubContainer.containerIpAddress
val port = stubContainer.getMappedPort(8080)
val stubServer = "http://$host:$port"
// Your functional test can now use the stubServer to drive functional and other scenarios!!
}
}In Step 02, we wrote our first pact which looked something like below:
@Pact(provider = "CustomerService", consumer = "AndroidClient")
fun getDetailsById(builder: PactDslWithProvider): RequestResponsePact {
return builder.given("A customer with an existing ID")
.uponReceiving("a request for customer details")
.path("/customers/1234")
.method("GET")
.headers(mapOf("Accept" to "application/json"))
.willRespondWith()
.headers(mapOf("Content-Type" to "application/json"))
.body(
PactDslJsonBody()
.stringType("firstName", "Test")
.stringType("lastName", "First")
)
.status(200)
.toPact()
}Pact advocates applying Postel's Law - Be conservative in what you do, be liberal in what you accept from others. In our context, this can be reworded as Be conservative in what you send (when composing the request), be liberal in what you accept (when processing the response). Keeping this guideline in mind, the pact above can be refactored to read:
@Pact(provider = "CustomerService", consumer = "AndroidClient")
fun getDetailsForExistingCustomerId(builder: PactDslWithProvider): RequestResponsePact {
return builder.given("an existing customer with a valid id")
.uponReceiving("a request for an existing customer id")
.matchPath("/customers/\\d+", "/customers/1234")
.method("GET")
.headers(mapOf(ACCEPT to APPLICATION_JSON))
.willRespondWith()
.headers(mapOf(CONTENT_TYPE to APPLICATION_JSON))
.body(
PactDslJsonBody()
.stringMatcher("firstName", "[A-Z][\\w\\s]+", "Test")
.stringMatcher("lastName", "[A-Z][\\w\\s]+", "First")
)
.status(SC_OK)
.toPact()
}Notice how the request and particularly the response now allows the first and last name attributes to match a regular expression as opposed to only accepting a hard-coded value. This and more such guidelines and gotchas are elaborated in the Pact documentation. It will be very useful to be mindful of these when creating more complex pacts for real-world use.
The steps are very similar to writing the first pact.
- Start with writing a
@Pact
@Pact(provider = "CustomerService", consumer = "AndroidClient")
fun getDetailsForNonExistentCustomerId(builder: PactDslWithProvider): RequestResponsePact {
return builder.given("a non-existent customer with an invalid id")
.uponReceiving("a request for a non-existent customer id")
.matchPath("/customers/\\d+", "/customers/112233")
.method("GET")
.headers(mapOf(ACCEPT to APPLICATION_JSON))
.willRespondWith()
.status(SC_NOT_FOUND)
.toPact()
}A few notable things in the code above:
- The
@Pactannotation needs to use the same values for theproviderand theconsumerattributes as the other pact we created. - The provider state (argument to the
builder.given(...)method) needs to be distinct from all other pacts for this consumer-provider combination. - The
pathargument is identical to the one specified in the other state we created in Step 02. With this being the case, the pact stub server will randomly return any one of the responses matching the request path. Usually, this is likely to not be the expected behavior. To avoid this, additionally qualify the request using thestateparameter. For an example, look atCustomerServiceFunctionalTests
In Step 05, we populated the provider database with data to match the state expectations of the interaction. However, this can get pretty cumbersome if your provider has quite a bit of expensive setup (it is another matter that your service probably shouldn't have too many dependencies. But we'll leave that discussion for some other time). An alternate implementation which mocks the CustomerRepository does not require this kind of (relatively) expensive database setup. This looks something like below:
public class AndroidClientMockContractTests extends AbstractAndroidClientContractTests {
@MockBean
private CustomerRepository repository;
@State("an existing customer with a valid id")
void pactWithAnExistingCustomer() {
when(repository.findById(1234L))
.thenReturn(Mono.just(new Customer(1234L, "Test", "First")));
}
}Look at AndroidClientIntegrationContractTests, AndroidClientMockContractTests and AbstractAndroidClientContractTests for more details.