logo logo

Consumer-Driven Contract Testing using Pact Java

Consumer Driven Contract Testing Using Pact Java

In the previous article we discussed how we can use Pact for consumer-driven testing using JavaScript. In this chapter, we will look at how we can use Pact for consumer-driven testing using Java.

Table of Contents

  1. Introduction to Consumer Contract Testing
  2. Consumer-Driven Contract Testing using Pact.js
  3. You’re here → Consumer-Driven Contract Testing using Pact Java
    1. Consumer Testing
    2. Verifying the Contract
  4. Consumer-Driven Contract Testing using Spring Cloud Contracts
  5. Event-Driven Architecture: How to Perform Contract Testing in Kafka/pubSub
  6. Integrating Contract Testing in Build Pipelines

Consumer-Driven Contract Testing using Pact Java

To demonstrate the concept of Consumer Driven Contracts, we have the following micro-services applications built using Spring Boot:

  • Date Provider MicroService – /provider/validDate – Validates whether the given date is a valid date or not.
  • Age Consumer MicroService – /age-calculate – Returns age of a person based on a given date.

Starting Date Provider MicroService which by default runs in port 8080:

mvn spring-boot:run -pl date-provider

Then start Age Consumer MicroService which by default runs in port 8081:

mvn spring-boot:run -pl age-consumer

Since the application is developed using Spring Boot in Java, we prefer to use pact-JVM for consumer-driven contract testing. Pact-JVM provides a DSL for defining contracts. In addition, pact-JVM offers good integration with test frameworks such as JUnit, Spock, ScalaTest, etc as well as with build tools such as Maven, Gradle, and sbt.

Let’s see how to write consumer and provider tests using Pact JVM 🚀

Consumer Testing

Consumer-Driven Contract testing begins with a consumer defining the contract. Before we start writing code, we have to add the following dependency to our project:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5</artifactId>
    <version>4.0.9</version>
    <scope>test</scope>
</dependency>

Than add below dependency for writing java 8 lambda DSL for use with JUnit to build consumer tests. A Lamda DSL for Pact is an extension for the pact DSL provided by pact-jvm-consumer.

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-java8</artifactId>
    <version>4.0.9</version>
    <scope>test</scope>
</dependency>

Consumer tests start with creating expectations on the mock HTTP server using fluent API. Let’s start with the stub:

@Pact(consumer = "ageConsumer")
public RequestResponsePact validDateFromProvider(PactDslWithProvider builder) {
    Map<String, String> headers = new HashMap<String, String>();
    headers.put("content-type", "application/json");

    return builder
            .given("valid date received from provider")
            .uponReceiving("valid date from provider")
            .method("GET")
            .queryMatchingDate("date", "2001-02-03")
            .path("/provider/validDate")
            .willRespondWith()
            .headers(headers)
            .status(200)
            .body(LambdaDsl.newJsonBody((object) -> {
                object.numberType("year", 2000);
                object.numberType("month", 8);
                object.numberType("day", 3);
                object.booleanType("isValidDate", true);
            }).build())
            .toPact();
}

The above code is quite similar to what we do with API mocks using WireMock. We can define the input which is HTTP GET method against the /provider/validDate path and the output is the below JSON body:

{
  "year": 2000,
  "month": 8,
  "day": 3,
  "isValidDate": true
}

In the above lambda DSL, we used numberType and booleanType generates a matcher that just checks the type whereas numberValue and booleanValue put a concrete value in the contract. Using matchers reduces tight coupling between consumers and producers. Values like 2000, 8, etc are example values returned by the mock server. The next part is the test:

@Test
@PactTestFor(pactMethod = "validDateFromProvider")
public void testValidDateFromProvider(MockServer mockServer) throws IOException {
HttpResponse httpResponse = Request.Get(mockServer.getUrl() + "/provider/validDate?date=2001-02-03")
            .execute().returnResponse();

assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(200);
    assertThat(JsonPath.read(httpResponse.getEntity().getContent(), "$.isValidDate").toString()).isEqualTo("true");
}

@PactTestFor annotation connects the Pact method with a test case. The last thing we need to add before we run the test is to add @ExtendsWidth and @PactTestFor annotation with the name of the provider.

@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "dateProvider", port = "1234")
public class PactAgeConsumerTest {

Maven command to execute the consumer test is:

mvn -Dtest=PactAgeConsumerTest test -pl age-consumer

The test will pass and the JSON file containing a contract has been generated in the target directory (target/pacts).

{
  "provider": {
    "name": "dateProvider"
  },
  "consumer": {
    "name": "ageConsumer"
  },
  "interactions": [
    {
      "description": "valid date from provider",
      "request": {
        "method": "GET",
        "path": "/provider/validDate",
        "query": {
          "date": [
            "2001-02-03"
          ]
        },
        "matchingRules": {
          "query": {
            "date": {
              "matchers": [
                {
                  "match": "date",
                  "date": "2001-02-03"
                }
              ],
              "combine": "AND"
            }
          }
        },
        "generators": {
          "body": {
            "date": {
              "type": "Date",
              "format": "2001-02-03"
            }
          }
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "content-type": "application/json",
          "Content-Type": "application/json; charset=UTF-8"
        },
        "body": {
          "month": 8,
          "year": 2000,
          "isValidDate": true,
          "day": 3
        },
        "matchingRules": {
          "body": {
            "$.year": {
              "matchers": [
                {
                  "match": "number"
                }
              ],
              "combine": "AND"
            },
            "$.month": {
              "matchers": [
                {
                  "match": "number"
                }
              ],
              "combine": "AND"
            },
            "$.day": {
              "matchers": [
                {
                  "match": "number"
                }
              ],
              "combine": "AND"
            },
            "$.isValidDate": {
              "matchers": [
                {
                  "match": "type"
                }
              ],
              "combine": "AND"
            }
          },
          "header": {
            "Content-Type": {
              "matchers": [
                {
                  "match": "regex",
                  "regex": "application/json(;\\s?charset=[\\w\\-]+)?"
                }
              ],
              "combine": "AND"
            }
          }
        }
      },
      "providerStates": [
        {
          "name": "valid date received from provider"
        }
      ]
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "3.0.0"
    },
    "pact-jvm": {
      "version": "4.0.9"
    }
  }
}

Every interaction has:

  • Description
  • Provider state – Allows the provider to set up a state.
  • Request – Consumer makes a request.
  • Response – Expected response from the provider.

Then the generated pact file is then published to pact broker by the consumer. Now it’s time for the producers to verify the contract messages shared via pact broker.

Verifying the Contract

In our case, the provider is a simple Spring boot application. Before we start writing code, we have to add the following dependency to our project:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-junit5</artifactId>
    <version>4.0.10</version>
</dependency>

We need to define a way for the provider to access the pact file in a pact broker. @PactBroker annotation takes the hostname and port number of the actual pact broker URL. With the @SpringBootTest annotation, Spring Boot provides a convenient way to start up an application context to be used in a test.

@Provider("dateProvider")
@Consumer("ageConsumer")
@PactBroker(host = "localhost", port = "8282")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PactAgeProviderTest {

We also have to tell pact where it can expect the provider API.

@BeforeEach
void before(PactVerificationContext context) {
    context.setTarget(new HttpTestTarget("localhost", port));
}

@LocalServerPort
private int port;

Then we publish all the verification results back to the pact broker by setting the environment variable.

@BeforeAll
static void enablePublishingPact() {
    System.setProperty("pact.verifier.publishResults", "true");
}

We inform the JUnit of how to perform the test like below:

@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
}

@State annotation in the method is responsible for setting up the system to the state expected by the contract.

@State("valid date received from provider")
public void validDateProvider() {
}

Maven command to execute the provider test is:

mvn -Dtest=PactAgeProviderTest test -pl date-provider

Any changes made by the provider like adding a new field or removal of an unused field in the contract won’t have any impact on existing consumers as they care only about the parameters or attributes in the existing contract.

Any changes made by the provider like removing a used field or renaming it in the contract will violate the contract and it makes consumers fail in production.

Adding a new interaction by the consumer generates a new pact file and the same needs to be verified by the provider in pact broker.

The entire working code snippets can be found here.

In the next chapter, we will see how to perform consumer-driven contract tests in an event-driven architecture.

Happy Testing 😎
Srinivasan Sekar & Sai Krishna

About the author

Srinivasan Sekar

Srinivasan Sekar is a Lead Consultant at ThoughtWorks. He loves contributing to Open Source. He is an Appium Member and Selenium Contributor as well. He worked extensively on testing various Mobile and Web Applications. He specializes in building automation frameworks. He has also spoken at various conferences including SeleniumConf, AppiumConf, SLASSCOM, BelgradeTestConf, QuestForQualityConf, and FOSDEM.

Leave a Reply

FacebookLinkedInTwitterEmail