You are currently viewing Consumer-driven contract testing with Pact

Consumer-driven contract testing with Pact

Consumer-driven contract testing will prevent the APIs you provide and consume from breaking unexpectedly. Without requiring you to do any action (aside from looking at your tests/CI builds).
This fosters further development and introducing (breaking!) features!

But how so?

In consumer-driven contract testing you create contracts between consumers and providers. If you break the contract you will notice!

Let’s look at a hands-on example

Imagine you have a set of the following microservices:

  • A customer service
  • A billing service
  • and a shipping service

And let’s say the customer service provides data of the following kind:

GET /customer/123
returns:

{
  "name": "Case",
  "firstName": "Justin",
  "address": {
    "street": "Mock Blvd. 42",
    "zip": "0815",
    "city": "Mocktown"
   },
   "creditLimit": 2000
}

Both, the billing and the shipping service need to access this endpoint.

service interaction
Interaction: Billing and shipping service call the endpoint from the customer service

But they consume different data:

  • The billing service only needs the creditLimit
  • The shipping service needs the name, firstName and the address block

We will use Pact now to define these requirements in the respective services.

For using Pact on the client side you’ll need to add the following dependency to your maven pom.xml:

<dependency>
  <groupId>au.com.dius</groupId>
  <artifactId>pact-jvm-consumer-junit_2.11</artifactId>
  <version>3.5.24</version>
  <scope>test</scope>
</dependency>

And a configuration where the pacts/contracts should be generated to:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <systemPropertyVariables>
    <pact.rootDir>target/generated-pacts</pact.rootDir>
    </systemPropertyVariables>
  </configuration>
</plugin>

Now that we have this, let’s write the consumer test for the billing service:

public class BillingServiceCustomerGatewayPactTest {

  @Rule
  public PactProviderRuleMk2 mockProvider
      = new PactProviderRuleMk2("customer_service_provider", "localhost", 8080, this);

  @Pact(consumer = "billing_service_consumer")
  public RequestResponsePact getCustomerForBilling(PactDslWithProvider builder) {
    val headers = new HashMap<String, String>();
    headers.put("Content-Type", "application/json");

    val body = new PactDslJsonBody();
    body.numberValue("creditLimit", 2000).closeObject();
    return builder
        .given("test GET customer for billing")
        .uponReceiving("GET REQUEST")
        .path("/customer/123")
        .method("GET")
        .willRespondWith()
        .status(200)
        .headers(headers)
        .body(body).toPact();
  }

  @PactVerification(fragment = "getCustomerForBilling")
  @Test
  public void verifyGetCustomerForBillingPact() {
    // Of course in a real world example this would be the
    // client/gateway class of the billing service and not just a RestTemplate
    val result = new RestTemplate()
        .getForObject("http://localhost:8080/customer/123", String.class);
    assertThat(result).isEqualTo("{\"creditLimit\":2000}");
  }
}

What does this do?

The part annotated with @Pact is the one which generates the contract. It reads like a RestAssured test, but it’s actually the other way around:
It’s used to set up a mock of the provider endpoint.
This mock runs on server and port specified by the @Rule.

Finally the @PactVerification annotated function is a normal test method.
But having it marked with this annotation will make use of the mock and make sure that it is called.

So far nothing special. Could have been done easier using normal means of mocking, right?

But wait until you see the provider side…

After executing this test on the consumer side (surprise: It’s green) you’ll see that a json has been generated in target/generated-pacts.
It should look like this: (billing_service_consumer-customer_service_provider.json)

{
    "provider": {
        "name": "customer_service_provider"
    },
    "consumer": {
        "name": "billing_service_consumer"
    },
    "interactions": [
        {
            "description": "GET REQUEST",
            "request": {
                "method": "GET",
                "path": "/customer/123"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "creditLimit": 2000
                }
            },
            "providerStates": [
                {
                    "name": "test GET customer for billing"
                }
            ]
        }
    ],
    "metadata": {
        "pactSpecification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.24"
        }
    }
}

What to do on the provider side?

Note: For this tutorial I will cheat a bit to be able to focus on consumer-driven contract testing and not mixing it up with test setup code.

Hence the controller in the customer service looks like this:

@RestController
public class CustomerController {

  @GetMapping(path = "/customer/123", produces = "application/json")
  public Customer getCustomer() {
    return Customer.builder().firstName("Justin").name("Case")
        .address(
            Address.builder().street("Mock Blvd. 42").zip("0815").city("Mocktown").build())
        .creditLimit(2000)
        .build();
  }
}

(and you can imagine how the Customer and Address classes look like, right?)

Then you add the following dependency to your maven pom.xml:

<dependency>
  <groupId>au.com.dius</groupId>
  <artifactId>pact-jvm-provider-spring_2.12</artifactId>
  <version>3.5.24</version>
  <scope>test</scope>
</dependency>

With that in scope you can write your provider test like this:

@RunWith(SpringRestPactRunner.class)
@Provider("customer_service_provider")
@PactFolder("src/test/resources/pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class CustomerServicePactTest {

  @LocalServerPort
  private int port;

  @TestTarget
  public final Target target = new SpringBootHttpTarget();

  @State("test GET customer for billing")
  public void testGetCustomerForBilling() {
    //Nothing to do here
  }

}

When you run it you will see an output like this:

Verifying a pact between billing_service_consumer and customer_service_provider
  Given test GET customer for billing
  GET REQUEST
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (OK)

What happens here?

The server side picks up the pact defined in the directory and creates a test out of it.
With the @State annotation you specify which provider state you want to test.

And of course you will have not only this file in the pact directory but also the ones from the shipping service!

Here is the code for the shipping service part

public class ShippingServiceCustomerGatewayPactTest {

  @Rule
  public PactProviderRuleMk2 mockProvider
      = new PactProviderRuleMk2("customer_service_provider", "localhost", 8080, this);

  @Pact(consumer = "shipping_service_consumer")
  public RequestResponsePact getCustomerForShipping(PactDslWithProvider builder) {
    val headers = new HashMap<String, String>();
    headers.put("Content-Type", "application/json");

    // Btw. there's no need to use the PactDsl here: You can use ObjectMapper too
    val body = new PactDslJsonBody();
    body.stringValue("name", "Case")
        .stringValue("firstName", "Justin")
        .object("address")
        .stringValue("city", "Mocktown")
        .stringValue("street", "Mock Blvd. 42")
        .stringValue("zip", "0815")
        .closeObject()
        .closeObject();
    return builder
        .given("test GET customer for shipping")
        .uponReceiving("GET REQUEST")
        .path("/customer/123")
        .method("GET")
        .willRespondWith()
        .status(200)
        .headers(headers)
        .body(body).toPact();
  }

  @PactVerification(fragment = "getCustomerForShipping")
  @Test
  public void verifyGetCustomerForShippingPact() {
    val result = new RestTemplate()
        .getForObject("http://localhost:8080/customer/123", String.class);
    assertThat(result).isNotNull();
  }
}

and copy the generated json file to src/test/resources/pacts/

On the provider side you just need to add the state for this:

@RunWith(SpringRestPactRunner.class)
@Provider("customer_service_provider")
@PactFolder("src/test/resources/pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class CustomerServicePactTest {

  @LocalServerPort
  private int port;

  @TestTarget
  public final Target target = new SpringBootHttpTarget();

  @State("test GET customer for billing")
  public void testGetCustomerForBilling() {
    //Nothing to do here
  }

  @State("test GET customer for shipping")
  public void testGetCustomerFoShipping() {
    //Here neither
  }
}

If you now have breaking changes on the provider side the tests will show you!

Let’s break the contract!

Try it with removing city from the Address class and running the test again…

The contract with the billing service is still fine

We broke the contract with the shipping service


But we broke the contract with the shipping service.

The console output will be like this:

Verifying a pact between shipping_service_consumer and customer_service_provider
  Given test GET customer for shipping
  GET REQUEST
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (FAILED)

Failures:

0) GET REQUEST returns a response which has a matching body
      $.address -> Expected a Map with at least 3 elements but received 2 elements

        Diff:

        {
        -    "city": "Mocktown",
            "street": "Mock Blvd. 42",

      $.address -> Expected city='Mocktown' but was missing

        Diff:

        {
        -    "city": "Mocktown",
            "street": "Mock Blvd. 42",

...

Pretty cool, right?

How to further improve this

There is still a little bit of manual work involved in the solution here:
When the contract on the consumer side changes, it needs to be copied to the pacts folder on the provider side.

While this may be sufficient for you it can be made in a more sophisticated way:
In a real world scenario you probably don’t want to copy the generated pact files from the consumers to the provider manually.
For this case Pact has the concept of a Pact broker, which is that you configure your build pipeline to push the contracts of clients to a place where it can be made available via a server application.
Then you don’t specify the pact folder, but the URL of the pact broker instead.

You can even make it multi-branch aware. Meaning you have different contracts for different git branches. Depending on your branching model it can be quite helpful to have the contracts differ, i.e. in feature branches and being able to break and repair things there, before merging back to your standard branch…

But setting this up involves a more complicated setup of your build pipeline and is out of the scope of this brief tutorial. Also you may consider it to be sufficient to have the pacts generated to a specific “shared” location in your network…

Why Pact and not spring-cloud-contract?

Spring-cloud-contract is a similar framework and, from an implementation side of view, not very different from pact.

But why do I prefer Pact?

Because Pact is usable from many languages aside from the JVM ecosystem:
There are Javascript, Python, Ruby, Go, .Net and other implementations.
Typically you have your microservice infrastructure (which may or may not all be JVM based) and one or more frontends talking to the microservices. So the contracts can be made from any of these…
For me this advantage is huge.

Wrap up

You learned in this tutorial how to set up consumer-driven contract testing with Pact (in a spring boot microservices environment).

The big advantage over other kinds of tests is, that it breaks your build when you break the contract with the consumers on the provider side.

This is pretty big, compared to usual mock tests, but it can’t replace End-to-end test, since you can’t test data semantics and are unable to write tests for unknown API consumers with it.

While a sophisticated setup with a Pact broker is preferable, in my opinion a simple approach like the one pictured here can still bring benefits for teams which manage only a few microservices.

How do you think about it? Feel free to discuss!

Leave a Reply