the/experts. Blog

Cover image for Contract Testing With Pact And Junit5
Joep Laarhoven
Joep Laarhoven

Posted on • Updated on

Contract Testing With Pact And Junit5

Contract testing is an excellent way to verify that two separate systems can communicate with each other using REST APIs and make sure the data is compatible with one another. This is done using contracts, both sides need to adhere to this contract to pass the tests.

Steps

  1. Libraries
  2. Prerequisites
  3. Generating Pact Contract
  4. Verify Contract
  5. Publish Contract
  6. Consumer Test
  7. Provider Test
  8. Conclusion

Libraries

The libraries being used for the generation of the contract and testing the consumer:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-consumer-junit5</artifactId>
    <version>4.0.3</version>
    <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

The libraries being used for provider testing:

<dependency>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-junit5</artifactId>
    <version>4.0.3</version>
    <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Prerequisites

Before we can do the contract testing it is important that both sides can reach the contract. This is done by deploying a contract to the contract broker, during this guide I will be using the broker of Pact itself. This can be deployed on Docker using the following docker-compose.yaml.

version: '3'

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

  broker_app:
    image: dius/pact-broker
    ports:
    - "80:80"
    links:
    - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: postgres
      PACT_BROKER_DATABASE_PASSWORD: password
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: postgres
Enter fullscreen mode Exit fullscreen mode

Generating Pact Contract

Pact is a tool for consumer-driven contract testing, this means the consumer (I.E. a front-end service) generates the contract based on the requests and responses.
To do this, create a new test file. Annotate this test class with @ExtendWith({PactConsumerTestExt.class}) and @PactTestFor(providerName = "animal_pact_test_provider") where animal_pact_test_provider is changed to your own provider name.
The next step is to initialize a hashmap with two strings as keys and values, this is where we are going to place the headers.
Now we can create the function which will generate the contract.

@Pact(consumer = "pact_test_consumer",
      provider = "animal_pact_test_provider")
public RequestResponsePact createFragment(PactDslWithProvider builder) {
    headers.putIfAbsent("Content-Type", "application/json");
    contract = builder
            .uponReceiving("A request for an animal")
            .method("GET")
            .path("/dieren/dier/paard").willRespondWith().status(HttpStatus.OK.value()).headers(this.headers)
            .body(this.getStubJsonBodyAsString())
            .toPact();
    return contract;
}
Enter fullscreen mode Exit fullscreen mode

For Pact to know what consumer and provider this contract is for, you can annotate the function with @Pact. The function returns the actual contract which will be saved in target/pacts. Using the PactDslWithProvider builder we will generate the contract based on the method, path, and body it will receive as a response.
The .uponReceiving("A request for an animal") function will set the state of this contract, which will be used later on during the provider test.
this.getStubJsonBodyAsString() is returning a string that contains the same body as the response body of the API.

private String getStubJsonBodyAsString() {
        return "{\"kleur\":\"zwart\",\"geluid\":\"Hiii!!!\",\"naam\":\"Black Beauty\",\"snoepje\":\"wortel\",\"ras\":\"Fries\",\"speeltje\":\"skippybal\",\"notitie\":\"Een Fries snoept graag wortel en speelt graag met een skippybal\"}";
    }
Enter fullscreen mode Exit fullscreen mode

Verify Contract

Before we can publish the contract to the broker we need to verify if the contract is built up correctly.

@Test
public void verifyContractForAnimalProvider(MockServer mockServer) throws IOException {
    HttpResponse httpResponse = Request.Get(mockServer.getUrl() + "/dieren/dier/" + "paard").execute().returnResponse();
    Assertions.assertEquals(httpResponse.getStatusLine().getStatusCode(), 200);
    Assertions.assertEquals(httpResponse.getFirstHeader("Content-Type").getValue(), this.headers.get("Content-Type"));
    String responseString = (new BasicResponseHandler()).handleResponse(httpResponse);
    System.out.println("response = " + responseString);
    Assertions.assertEquals(responseString, this.getStubJsonBodyAsString());
}
Enter fullscreen mode Exit fullscreen mode

To do this, we are going to mock the server and assert that the return code is 200, the headers are equal to the hashmap we created earlier, and the body is equal to this.getStubJsonBodyAsString().

Publish Contract

Once the contract has been verified, we can publish the contract to the Pact broker we set up with Docker. To do this, we need to install a new Maven plugin.

<plugin>
    <groupId>au.com.dius</groupId>
    <artifactId>pact-jvm-provider-maven</artifactId>
    <version>4.0.3</version>
    <configuration>
        <pactDirectory>target/pacts</pactDirectory>
        <pactBrokerUrl>http://localhost:80</pactBrokerUrl>
        <projectVersion>1.0.105</projectVersion>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Using this plugin, we can publish the contract using the mvn pact:publish command.
Now go into your browser and go to localhost:80, a similar screen to the following image should show up.
Pact Broker

Consumer Test

Testing the consumer is fairly easy using Junit5. Create a new test class and annotate this with:

  • @Provider("animal_pact_test_provider") replace animal_pact_test_provider with the name of your own provider.
  • @Consumer("pact_test_consumer") replace pact_test_consumer with the name of your own consumer.
  • @PactBroker(host = "localhost", port = "80") replace the host and port with the IP and port of your broker.
@Provider("animal_pact_test_provider")
@Consumer("pact_test_consumer")
@PactBroker(host = "localhost", port = "80")
public class ProviderServiceTest {
}
Enter fullscreen mode Exit fullscreen mode

Next, we are setting up the PactVerficationContext to verify the interaction in the actual test. To do this create a HttpTestTarget with the URL of your consumer application (I.E. front-end) and set this TestTarget to the context.

@BeforeEach
public void setTarget(PactVerificationContext context) {
    HttpTestTarget target = new HttpTestTarget("localhost", 7171);
    context.setTarget(target);
}
Enter fullscreen mode Exit fullscreen mode

The actual consumer test function will be annotated with @TestTemplate and @ExtendWith(PactVerificationInvocationContextProvider.class). As an argument add PactVerificationContext context which will be used to verify the contract.

@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
public void whenRunTestTemplateWillVerifyInteraction(PactVerificationContext context) {
    context.verifyInteraction();
}
Enter fullscreen mode Exit fullscreen mode

The actual assertions of the return code, headers, and body are done by the .verifyInteraction(). The output should similar to the image below. Consumer Test Output

Provider Test

Just like the consumer test, create a new test class and annotate this with:

  • @Provider("animal_pact_test_provider") replace animal_pact_test_provider with the name of your own provider.
  • @PactBroker(host = "localhost", port = "80") replace the host and port with the IP and port of your broker. This time you don't have to annotate the class with @Consumer.
@Provider("animal_pact_test_provider")
@PactBroker(host = "localhost", port = "80")
public class PactproviderJunit5Tests {
}
Enter fullscreen mode Exit fullscreen mode

Define a static ConfigurableWebApplicationContext. this will be used to start the application before the first test is run.

private static ConfigurableWebApplicationContext application;
@BeforeAll
public static void start() {
    application = (ConfigurableWebApplicationContext) SpringApplication.run(PactproviderApplication.class);
}
Enter fullscreen mode Exit fullscreen mode

This makes sure the application is reachable by the target. The target is set in the same way as for the consumer.

@BeforeEach
void before(PactVerificationContext context) {
    context.setTarget(new HttpTestTarget("localhost", 7878, "/"));
}
Enter fullscreen mode Exit fullscreen mode

The provider test is also done in the same way as the consumer test, the difference here is the context verification this time requires a function annotated with @State("A request for an animal") where A request for an animal is replaced by whatever is at description in your pact contract. This function can be empty.

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

@State("A request for an animal")
public void toRequestState() { }
Enter fullscreen mode Exit fullscreen mode

If everything went right, the output should look like this:Result Provider Test

Conclusion

Making contract testing is a smart way to make sure both sides of the API have the same data. Pact is a good way to do this, especially because with Pact and Junit5 a lot of trouble is saved by the functions like the verifyInteraction function within the library.

Discussion (0)