Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this quick article, we’ll be looking at the concept of Consumer-Driven Contracts.

We’ll be testing integration with an external REST service through a contract that we define using the Pact library. That contract can be defined by the client, then picked up by the provider and used for development of its services.

We’ll also create tests based on the contract for both the client and provider applications.

2. What Is Pact?

Using Pact, we can define consumer expectations for a given provider (that can be an HTTP REST service) in the form of a contract (hence the name of the library).

We’re going to set up this contract using the DSL provided by Pact. Once defined, we can test interactions between consumers and the provider using the mock service that is created based on the defined contract. Also, we’ll test the service against the contract by using a mock client.

3. Maven Dependency

To get started we’ll need to add Maven dependency to pact-jvm-consumer-junit5_2.12 library:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5_2.12</artifactId>
    <version>3.6.3</version>
    <scope>test</scope>
</dependency>

4. Defining a Contract

When we want to create a test using Pact, first we need to annotate our test class with the provider that will be used:

@PactTestFor(providerName = "test_provider", hostInterface="localhost")
public class PactConsumerDrivenContractUnitTest

We’re passing the provider name and host on which the server mock (which is created from the contract) will be started.

Let’s say that service has defined the contract for two HTTP methods that it can handle.

The first method is a GET request that returns JSON with two fields. When the request succeeds, it returns a 200 HTTP response code and the Content-Type header for JSON.

Let’s define such a contract using Pact.

We need to use the @Pact annotation and pass the consumer name for which the contract is defined. Inside of the annotated method, we can define our GET contract:

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

    return builder
      .given("test GET")
        .uponReceiving("GET REQUEST")
        .path("/pact")
        .method("GET")
      .willRespondWith()
        .status(200)
        .headers(headers)
        .body("{\"condition\": true, \"name\": \"tom\"}")
        (...)
}

Using the Pact DSL we define that for a given GET request we want to return a 200 response with specific headers and body.

The second part of our contract is the POST method. When the client sends a POST request to the path /pact with a proper JSON body it returns a 201 HTTP response code.

Let’s define such contract with Pact:

(...)
.given("test POST")
.uponReceiving("POST REQUEST")
  .method("POST")
  .headers(headers)
  .body("{\"name\": \"Michael\"}")
  .path("/pact")
.willRespondWith()
  .status(201)
.toPact();

Note that we need to call the toPact() method at the end of the contract to return an instance of RequestResponsePact.

4.1. Resulting Pact Artifact

By default, Pact files will be generated in the target/pacts folder. To customize this path, we can configure the maven-surefire-plugin:

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

The Maven build will generate a file called test_consumer-test_provider.json in the target/mypacts folder which contains the structure of the requests and responses:

{
    "provider": {
        "name": "test_provider"
    },
    "consumer": {
        "name": "test_consumer"
    },
    "interactions": [
        {
            "description": "GET REQUEST",
            "request": {
                "method": "GET",
                "path": "/"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "condition": true,
                    "name": "tom"
                }
            },
            "providerStates": [
                {
                    "name": "test GET"
                }
            ]
        },
        {
            "description": "POST REQUEST",
            ...
        }
    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.6.3"
        }
    }
}

5. Testing the Client and Provider Using the Contract

Now that we have our contract, we can use to create tests against it for both the client and the provider.

Each of these tests will use a mock of its counterpart which is based on the contract, meaning:

  • the client will use a mock provider
  • the provider will use a mock client

Effectively, the tests are done against the contract.

5.1. Testing the Client

Once we defined the contract we can test interactions with the service that will be created based on that contract. We can create normal JUnit test but we need to remember to put the @PactTestFor annotation at the beginning of the test.

Let’s write a test for the GET request:

@Test
@PactTestFor
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {
 
    // when
    ResponseEntity<String> response = new RestTemplate()
      .getForEntity(mockProvider.getUrl() + "/pact", String.class);

    // then
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
    assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

The @PactTestFor annotation takes care of starting the HTTP service, and can be put either on the test class, or on the test method. In the test, we only need to send the GET request and assert that our response complies with the contract.

Let’s add the test for the POST method call as well:

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";

// when
ResponseEntity<String> postResponse = new RestTemplate()
  .exchange(
    mockProvider.getUrl() + "/create",
    HttpMethod.POST,
    new HttpEntity<>(jsonBody, httpHeaders), 
    String.class
);

//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);

As we can see, the response code for the POST request is equal to 201 – exactly as it was defined in the Pact contract.

As we were using the @PactTestFor() annotation, the Pact library is starting the web server based on the previously defined contract before our test case.

5.2. Testing the Provider

The second step of our contract verification is creating a test for the provider using a mock client based on the contract.

Our provider implementation will be driven by this contract in TDD fashion.

For our example, we’ll use a Spring Boot REST API.

First, to create our JUnit test, we’ll need to add the pact-jvm-provider-junit5_2.12 dependency:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-junit5_2.12</artifactId>
    <version>3.6.3</version>
</dependency>

This allows us to create a JUnit test specifying the provider name and the location of the Pact artifact:

@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderLiveTest {
    //...
}

For this configuration to work, we have to place the test_consumer-test_provider.json file in the pacts folder of our REST service project.

Next, for writing Pact verification tests with JUnit 5, we need to use PactVerificationInvocationContextProvider with the @TestTemplate annotation. We’ll need to pass it the PactVerificationContext parameter, which we’ll use to set the target Spring Boot application details:

private static ConfigurableWebApplicationContext application;

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

@BeforeAll
public static void start() {
    application = (ConfigurableWebApplicationContext) SpringApplication.run(MainApplication.class);
}

@BeforeEach
void before(PactVerificationContext context) {
    context.setTarget(new HttpTestTarget("localhost", 8082, "/spring-rest"));
}

Finally, we’ll specify the states in the contract that we want to test:

@State("test GET")
public void toGetState() { }

@State("test POST")
public void toPostState() { }

Running this JUnit class will execute two tests for the two GET and POST requests. Let’s take a look at the log:

Verifying a pact between test_consumer and test_provider
  Given test GET
  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)

Verifying a pact between test_consumer and test_provider
  Given test POST
  POST REQUEST
    returns a response which
      has status code 201 (OK)
      has a matching body (OK)

Note that we haven’t included the code for creating a REST service here. The full service and test can be found in the GitHub project.

6. Conclusion

In this quick tutorial, we had a look at Consumer Driven Contracts.

We created a contract using the Pact library. Once we defined the contract, we were able to test the client and service against the contract and assert that they comply with the specification.

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!