CODEX

Consumer-driven testing using Pact.io

I heard about contract testing as an alternative to integration testing and finally decided to learn its concepts and try it out using Pact.io.

Luís Soares
CodeX

--

How do you ensure multiple apps and services of the same system play well together?
You could run end-to-end testing but it’s nearly impossible to test all the combinations. Also, the effective pinpointing of problems would be very low.
You could also test in isolation, on a per-app basis by simulating the other services' replies, but since they can evolve independently, the safety net is low as each set of tests may pass although the integration is broken.
Finally, integration testing is the typical solution because tests are run against the depended-on services. This looks promising, but it can quickly bring up problems:

  • Apps/services may be developed by different teams, which makes it harder to integrate them all into one CI pipeline. Even worse, it may render local testing impractical.
  • Launching multiple services can make the tests slow with time because we have to spin them up multiple times during the tests. Quick feedback is essential in automated testing but that is easily harmed with integration testing.

Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests. Pact.io introduction

Faced with this, we can ask “what do service/app A and B have in common?”, “do I need to run both to confirm they are properly speaking?”. This is where Pact testing shines because we only test the overlap between A and B, which is called a pact or a contract. We test the provider’s against all its consumers’ contracts.

Contract testing makes sense when you control both sides of the contract, especially if they belong to different teams in the same company.

Contract testing is a technique for testing an integration point by checking each application in isolation to ensure the messages it sends or receives conform to a shared understanding that is documented in a “contract”. Pact Docs

How Pact works

📝 I decided to use Kotlin to try Pact.io, but it supports multiple languages/runtimes.

Show me the code

Pact supports a lot of use cases and combinations, but the only way to learn it is to start with a basic and typical example. Let’s say our users’ app depends on another company service to retrieve profile information. The concepts we need to define before starting are consumer and provider. The consumer is whoever calls an API; the provider is the API. In our case, the consumer will be the Users’ service; the provider is the Profiles’ service.

The consumer

Contract testing is also known as consumer-driven contract testing, which means we’ll start with the consumer. Start by adding the dependency:

testImplementation("au.com.dius:pact-jvm-consumer-junit5:4.+")

For the sake of the sample exercise, we’ll hardcode a place where the contracts are to be written:

tasks.withType<Test> {
useJUnitPlatform()
systemProperty("pact.rootDir", "src/main/resources/pact-tests")
}

📝 In a real project, we’d probably use a Pact broker, which is a central entity that manages contracts. Alternatively, we could manually copy the contracts elsewhere so they’re available to the provider.

Now it’s time to create a test with our expectations:

The first method (create pact) represents a simulation of how the provider is supposed to work in regard to a certain call (it resembles using a simulator for HTTP-based APIs like MockServer or WireMock). It behaves like a JUnit BeforeAll annotation where you do the setup.

The second method (get user) is the test itself. At this point, it assumes the fake API is running, so you can just make network calls in your gateways against the fake API base URL (mockServer.getUrl()).

After the test is green, go to the pact folder (/src/main/resources/pact-tests) and check the generated contract:

Take a look at the JSON: it describes request/response pairs (interactions) that as a whole represent a contract this consumer expected to be fulfilled.

The provider

On our Profiles service, we need to add the corresponding provider dependency:

testImplementation("au.com.dius.pact.provider:junit5:4.+")

Let’s look at our test:

The idea here is to run all the contracts that were assigned to the provider (by multiple consumers). Notice that we have hardcoded the pact folder which may not be realistic. The plan is to launch our API at port 4444 (notice the invocation to main()) and then tell Pact where it was launched so the magic can happen. Here’s the actual API service (using Javalin) for the sake of reference:

That’s all. Hope you liked it. I’m convinced by the power of binding tests to contracts (as in real-life contracts) since it gives me the safety net I need. You can find all the code in my Clean Architecture project. I recommend reading Pact.io documentation which is very beginner-friendly.

--

--

Luís Soares
CodeX
Writer for

I write about automated testing, Lean, TDD, CI/CD, trunk-based dev., user-centric dev, domain-centric arch, coding good practices,