Contract Testing using Pact with Java

Contract Testing using Pact with Java

In a microservices architecture, where many services can communicate with each other, through HTTP requests or messages, it is very important to ensure all the interactions will work as expected once the services are deployed, this can be done with integration testing where the services in question are deployed to some test environment, but it can easily become a difficult and expensive task, this is where contract testing comes into the picture, which is a technique to check the interaction points between two applications (a consumer and a provider) in isolation, through the definition of a contract.

Let's define a sample API for placing orders in a system, we are going to create a consumer client library and a provider application that will respond to client requests.

The Pact Framework is a code-first consumer-driven contract testing tool (contrary to API-first where the contract is defined using a specification like OpenAPI), where the contract is generated when the consumer tests are executed, so we will start coding the consumer application first.

Consumer Order API

The consumer application will consist of two classes mainly:

  • An Order model class to represent the data returned by the provider.
  • An OrderApiClient class to make the actual HTTP requests.

Let’s start with the Order class:

public class Order {
    private int id;
    private List<Item> items;

    // Setters and Getters
}

And this is the Item class to represent a list of items in the order:

public class Item {
    private String name;
    private int quantity;
    private int value;

    // Setters and Getters
}

Next is to implement the OrderApiClient class, we can use any HTTP client library available for java, for this example we will use retrofit. We need to create an interface that represents the order service:

import retrofit2.Call;
import retrofit2.http.GET;
import java.util.List;

public interface OrderService {
    @GET("orders")
    Call<List<Order>> getOrders();
}

Now let’s use this interface in the OrderApiClient class to make the actual HTTP call:

import retrofit2.Response;
import retrofit2.Retrofit;

import java.io.IOException;
import java.util.List;

public class OrderApiClient {
    private final OrderService orderService;

    public OrderApiClient(String url) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(JacksonConverterFactory.create())
                .build();

        orderService = retrofit.create(OrderService.class);
    }

    public List<Order> fetchOrders() throws IOException {
        Response<List<Order>> response = orderService.getOrders().execute();
        return response.body();
    }
}

We need to write the unit test for the OrderApiClient to validate the interaction with the Order API provider, using pact. There is an important decision to make at this point, as there are different versions of the pact specification, at the time of this writing, the JVM Pact implementation actively supports versions V3, V4, and V4 + plugins, for this tutorial we are going to use V4 which branch is 4.3.x, so let’s import the latest version of the junit5 consumer library from this branch.

testImplementation 'au.com.dius.pact.consumer:junit5:4.3.7'

Or for a maven project:

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

We need to create a class and add the PactConsumerTestExt extension to test with JUnit 5:

import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(PactConsumerTestExt.class)
public class OrderApiClientPactTest {
    
}

To define our mock provider, we need to create a method annotated with @Pact and return a V4Pact object that represents the interaction expectations. The pact consumer library provides a DSL (Domain Specific Language) for defining the expected response from the provider, the usage details can be found here, for this example, we will use the Lambda DSL as it is less error-prone and easier to read:

import static au.com.dius.pact.consumer.dsl.LambdaDsl.*;
...

@Pact(provider = "order_provider", consumer = "order_consumer")
public V4Pact listOfOrdersPact(PactDslWithProvider builder) {
    return builder
            .given("there are orders")
            .uponReceiving("a request for orders")
            .path("/orders")
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(Map.of("Content-Type", "application/json"))
            .body(newJsonArrayMinLike(1, a -> a.object(o -> {
                o.id("id");
                o.eachLike("items", i -> {
                    i.stringType("name");
                    i.numberType("quantity");
                    i.numberType("value");
                });
            })).build())
            .toPact(V4Pact.class);
}

In the above example, the given method defines the provider's expected state, this will be important when validating the contract on the provider side, we will get to that; in this case, the state is “there are orders”, so it is expected to at least one order to be returned.

We can define many @Pact methods as needed, the below example represents the scenario when “there are no orders”, so an empty array is expected:

@Pact(provider = "order_provider", consumer = "order_consumer")
public V4Pact noOrdersPact(PactDslWithProvider builder) {
    return builder
            .given("there are no orders")
            .uponReceiving("a request for orders")
            .path("/orders")
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(Map.of("Content-Type", "application/json"))
            .body("[]")
            .toPact(V4Pact.class);
}

Now we can write test methods that use these pacts and validates the response, the method needs to be annotated with the @PactTestFor, this annotation allows us to set up and control the mock server that will respond to our client request, in this case, we will use the pactMethod attribute to pass the name of the method where the pact we want to use was defined. Also, we can get injected a MockServer object to get information like the base URL to pass that to our client class:

@Test
@PactTestFor(pactMethod = "listOfOrdersPact")
void runHttpTest(MockServer mockServer) throws IOException {
    var client = new OrderApiClient(mockServer.getUrl());
    var orders = client.fetchOrders();
    Assertions.assertNotNull(orders);
    Assertions.assertFalse(orders.isEmpty());
    Assertions.assertNotNull(orders.get(0).getItems());
    Assertions.assertFalse(orders.get(0).getItems().isEmpty());
}

Once the test runs and passes successfully, a json file containing the interactions and the matching rules will be generated, by default, in the build/pacts directory (or target/pacts for maven), this is our contract file that can be shared with the person or team maintaining the Order API Provider.

Sharing the contract with the provider

Now that the contract pact document is created, it needs to be shared with the provider application to validate and confirms that all the expectations are met.

Although there are different ways to share the contract, the recommended method is to use a pact broker, the pact broker is an application for sharing the pact contract and verifying results, and it can be integrated into CI/CD pipelines. For testing purposes, we can use the below docker-compose file to start a pact broker server, more information and examples can be found in the pact-broker-docker Github repository.

version: "3"

services:
  postgres:
    image: postgres
    healthcheck:
      test: psql postgres --command "select 1" -U postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: postgres

  pact-broker:
    image: pactfoundation/pact-broker:latest
    ports:
      - "9292:9292"
    depends_on:
      - postgres
    environment:
      PACT_BROKER_PORT: "9292"
      PACT_BROKER_DATABASE_URL: "postgres://postgres:password@postgres/postgres"
      PACT_BROKER_LOG_LEVEL: INFO
      PACT_BROKER_SQL_LOG_LEVEL: DEBUG
      PACT_BROKER_BASIC_AUTH_USERNAME: "${PACT_BROKER_BASIC_AUTH_USERNAME:-}"
      PACT_BROKER_BASIC_AUTH_PASSWORD: "${PACT_BROKER_BASIC_AUTH_PASSWORD:-}"
      PACT_BROKER_PUBLIC_HEARTBEAT: "${PACT_BROKER_PUBLIC_HEARTBEAT:-false}"
      PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "10"

In order to publish the contract to the pact broker, we need to add the pact plugin to our consumer project and set the pact broker URL, user, and password; for gradle:

plugins {
    id "au.com.dius.pact" version "4.3.6"
}
...
pact {
    publish {
        pactBrokerUrl = 'http://localhost:9292/'
        pactBrokerUsername = 'user'
        pactBrokerPassword = 'password'
    }
}

And for maven:

<plugin>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>maven</artifactId>
    <version>4.3.6</version>
    <configuration>
      <pactBrokerUrl>http://localhost:9292/</pactBrokerUrl>
      <pactBrokerUsername>user</pactBrokerUsername>
      <pactBrokerPassword>password</pactBrokerPassword>
    </configuration>
</plugin>

If a user and password were defined for the pact broker server, use the pactBrokerUsername and pactBrokerPassword parameters, otherwise, they can be omitted.

Once the pact broker is running with docker-compose up, we can now execute gradle pactPublish or mvn pact:publish to publish the pact contract to the broker. If we go to http://localhost:9292/ in the browser, we will see the published contract.

pact_broker.png

Testing the Order API provider project

Let’s implement the Order API provider project using spring boot starter web, it can be created with spring initialize. We can copy over the Order and Item model classes from the consumer project. An OrdersController to implement the /orders endpoint may look like this:

@RestController
public class OrdersController {
    private final OrdersRepository ordersRepository;

    @Autowired
    public OrdersController(OrdersRepository ordersRepository) {
        this.ordersRepository = ordersRepository;
    }

    @GetMapping("/orders")
    List<Order> getAllOrders() throws IOException {
        return ordersRepository.getOrders();
    }
}

In a real-life project, the OrdersRepository class may be a JpaRepository or something similar to fetch the orders from some data source, but for our sample project, it is just a component bean that returns a list of orders from a json file:

@Component
public class OrdersRepository {
    private final ObjectMapper objectMapper = new ObjectMapper();

    public List<Order> getOrders() throws IOException {
        URL resource = getClass().getClassLoader().getResource("orders.json");
        return objectMapper.readValue(resource, new TypeReference<>() {
        });
    }
}

Now we need to write a pact verification test, there is a library for Spring/JUnit5 that we can add to our project:

testImplementation 'au.com.dius.pact.provider:junit5spring:4.3.7'

This library provides an Invocation Context Provider that can be used with the @TestTemplate annotation, a test will be generated for each interaction. At the class level, we add the @Provider annotation to define the provider name, in our case "order_provider", and the @PactBroker annotation to point to the source of the pacts, by default is going to read the “*pactbroker.**” system properties that we can define in an application.yml or application.properties file in our application.

In the test method, we add @ExtendWith(PactVerificationSpringProvider.class) and a PactVerificationContext parameter where we need to call the verifyInteraction() method:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Provider("order_provider")
@PactBroker
public class ContractVerificationTest {
    @TestTemplate
    @ExtendWith(PactVerificationSpringProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }
}

As mentioned, we need to set the pactbroker properties in the application.yml or application.properties file:

pactbroker:
  url: http://localhost:9292
  auth:
    username: user
    password: password

With the pact broker running, we can try to execute our test, but it will fail with a couple of errors, if we look at the generated report, we will find more details:

pact_broker_errors.png

Click on one of the errors to see the full stack trace, the root cause will be something like:

Caused by: au.com.dius.pact.provider.junitsupport.MissingStateChangeMethod: Did not find a test class method annotated with @State("there are no orders") 
for Interaction "a request for orders" 
with Consumer "order_consumer"

The pact runner is complaining because we didn’t set up the expected state that the provider needs to be in before verifying a given interaction, this was defined by the consumer with the given(””) method when the pact was created.

We can define methods annotated with the @State annotation, there we configure our mocks to simulate the expected state.

@State("there are orders")
public void thereAreOrders() throws IOException {
    Mockito.when(ordersRepository.getOrders()).thenReturn(getOrdersFromFile("orders.json"));
}

@State("there are no orders")
public void thereAreNoOrders() throws IOException {
    Mockito.when(ordersRepository.getOrders()).thenReturn(getOrdersFromFile("no_orders.json"));
}

With the @State methods in place, we can run the tests again, if the pacts verification are successful now, the test report should be updated:

pact_report_success.png

Finally, we need to publish the verification results to the pact broker, to do that, we need to set a couple of system properties, pact.provider.version and pact.verifier.publishResults, in the test JVM, these properties are set in build.gradle:

test {
    useJUnitPlatform()
    systemProperty 'pact.provider.version', project.version
    systemProperty 'pact.verifier.publishResults', 'true'
}

Or with maven in the pom.xml:

<build>
	<plugins>
	  <plugin>
		<artifactId>maven-surefire-plugin</artifactId>
		<version>2.22.2</version>
		  <configuration>
			  <systemPropertyVariables>
				  <pact.provider.version>${project.version}</pact.provider.version>
				  <pact.verifier.publishResults>true</pact.verifier.publishResults>
			  </systemPropertyVariables>
		  </configuration>
	  </plugin>
    ...
  </plugins>
</build>

If we run the tests again, the results are published now to the pact broker.

pact_results_published.png

Although we may want to implement actual integration testing at some point, contract testing can be an earlier line of defense in finding issues that can be easily integrated in our CI/CD pipelines.

Find the complete code example here.