Testing microservices with consumer-driven contracts

duration 30 minutes
New

Prerequisites:

Learn how to test Java microservices with consumer-driven contracts in Open Liberty.

What you’ll learn

With a microservices-based architecture, you need robust testing to ensure that microservices that depend on one another are able to communicate effectively. Typically, to prevent multiple points of failure at different integration points, a combination of unit, integration, and end-to-end tests are used. While unit tests are fast, they are less trustworthy because they run in isolation and usually rely on mock data.

Integration tests address this issue by testing against real running services. However, they tend to be slow as the tests depend on other microservices and are less reliable because they are prone to external changes.

Usually, end-to-end tests are more trustworthy because they verify functionality from the perspective of a user. However, a graphical user interface (GUI) component is often required to perform end-to-end tests, and GUI components rely on third-party software, such as Selenium, which requires heavy computation time and resources.

What is contract testing?

Contract testing bridges the gaps among the shortcomings of these different testing methodologies. Contract testing is a technique for testing an integration point by isolating each microservice and checking whether the HTTP requests and responses that the microservice transmits conform to a shared understanding that is documented in a contract. This way, contract testing ensures that microservices can communicate with each other.

Pact is an open source contract testing tool for testing HTTP requests, responses, and message integrations by using contract tests.

The Pact Broker is an application for sharing Pact contracts and verification results. The Pact Broker is also an important piece for integrating Pact into continuous integration (CI) and continuous delivery (CD) pipelines.

The two microservices you will interact with are called system and inventory. The system microservice returns the JVM system properties of its host. The inventory microservice retrieves specific properties from the system microservice.

You will learn how to use the Pact framework to write contract tests for the inventory microservice that will then be verified by the system microservice.

Additional prerequisites

Before you begin, Docker needs to be installed. For installation instructions, refer to the official Docker documentation. You will deploy the Pact Broker in a Docker container.

Start your Docker daemon before you proceed.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-contract-testing.git
cd guide-contract-testing

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Starting the Pact Broker

Run the following command to start the Pact Broker:

docker-compose -f "pact-broker/docker-compose.yml" up -d --build

When the Pact Broker is running, you’ll see the following output:

Creating pact-broker_postgres_1 ... done
Creating pact-broker_pact-broker_1 ... done

Go to the http://localhost:9292/ URL to confirm that you can access the user interface (UI) of the Pact Broker, as shown in the following image:

Pact Broker webpage


You can refer to the official Pact Broker documentation for more information about the components of the Docker Compose file.

Implementing pact testing in the inventory service

Navigate to the start/inventory directory to begin.

When you run Open Liberty in development mode, known as dev mode, the server listens for file changes and automatically recompiles and deploys your updates whenever you save a new change. Run the following goal to start Open Liberty in dev mode:

mvn liberty:dev

After you see the following message, your application server in dev mode is ready:

************************************************************************
*    Liberty is running in dev mode.

Dev mode holds your command-line session to listen for file changes. Open another command-line session to continue, or open the project in your editor.

InventoryPactIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2021 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - initial API and implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13
 14package io.openliberty.guides.inventory;
 15
 16import au.com.dius.pact.consumer.dsl.PactDslJsonArray;
 17import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
 18import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
 19import au.com.dius.pact.consumer.junit.PactProviderRule;
 20import au.com.dius.pact.consumer.junit.PactVerification;
 21import au.com.dius.pact.core.model.RequestResponsePact;
 22import au.com.dius.pact.core.model.annotations.Pact;
 23
 24import org.junit.Rule;
 25import org.junit.Test;
 26
 27import static org.junit.Assert.assertEquals;
 28
 29import java.util.HashMap;
 30import java.util.Map;
 31
 32public class InventoryPactIT {
 33  // tag::mockprovider[]
 34  @Rule
 35  public PactProviderRule mockProvider = new PactProviderRule("System", this);
 36  // end::mockprovider[]
 37
 38  // tag::pact[]
 39  @Pact(consumer = "Inventory")
 40  // end::pact[]
 41  // tag::builder[]
 42  public RequestResponsePact createPactServer(PactDslWithProvider builder) {
 43    Map<String, String> headers = new HashMap<String, String>();
 44    headers.put("Content-Type", "application/json");
 45
 46    return builder
 47      // tag::given[]
 48      .given("wlp.server.name is defaultServer")
 49      // end::given[]
 50      .uponReceiving("a request for server name")
 51      .path("/system/properties/key/wlp.server.name")
 52      .method("GET")
 53      .willRespondWith()
 54      .headers(headers)
 55      .status(200)
 56      .body(new PactDslJsonArray().object()
 57        .stringValue("wlp.server.name", "defaultServer"))
 58      .toPact();
 59  }
 60  // end::builder[]
 61
 62  @Pact(consumer = "Inventory")
 63  public RequestResponsePact createPactEdition(PactDslWithProvider builder) {
 64    Map<String, String> headers = new HashMap<String, String>();
 65    headers.put("Content-Type", "application/json");
 66
 67    return builder
 68      .given("Default directory is true")
 69      .uponReceiving("a request to check for the default directory")
 70      .path("/system/properties/key/wlp.user.dir.isDefault")
 71      .method("GET")
 72      .willRespondWith()
 73      .headers(headers)
 74      .status(200)
 75      .body(new PactDslJsonArray().object()
 76        .stringValue("wlp.user.dir.isDefault", "true"))
 77      .toPact();
 78  }
 79
 80  @Pact(consumer = "Inventory")
 81  public RequestResponsePact createPactVersion(PactDslWithProvider builder) {
 82    Map<String, String> headers = new HashMap<String, String>();
 83    headers.put("Content-Type", "application/json");
 84
 85    return builder
 86      .given("version is 1.1")
 87      .uponReceiving("a request for the version")
 88      .path("/system/properties/version")
 89      .method("GET")
 90      .willRespondWith()
 91      .headers(headers)
 92      .status(200)
 93      .body(new PactDslJsonBody()
 94        .decimalType("system.properties.version", 1.1))
 95      .toPact();
 96  }
 97
 98  @Pact(consumer = "Inventory")
 99  public RequestResponsePact createPactInvalid(PactDslWithProvider builder) {
100
101    return builder
102      .given("invalid property")
103      .uponReceiving("a request with an invalid property")
104      .path("/system/properties/invalidProperty")
105      .method("GET")
106      .willRespondWith()
107      .status(404)
108      .toPact();
109  }
110
111  @Test
112  // tag::verification[]
113  @PactVerification(value = "System", fragment = "createPactServer")
114  // end::verification[]
115  public void runServerTest() {
116    // tag::mockTest[]
117    String serverName = new Inventory(mockProvider.getUrl()).getServerName();
118    // end::mockTest[]
119    // tag::unitTest[]
120    assertEquals("Expected server name does not match",
121      "[{\"wlp.server.name\":\"defaultServer\"}]", serverName);
122    // end::unitTest[]
123  }
124
125  @Test
126  @PactVerification(value = "System", fragment = "createPactEdition")
127  public void runEditionTest() {
128    String edition = new Inventory(mockProvider.getUrl()).getEdition();
129    assertEquals("Expected edition does not match",
130      "[{\"wlp.user.dir.isDefault\":\"true\"}]", edition);
131  }
132
133  @Test
134  @PactVerification(value = "System", fragment = "createPactVersion")
135  public void runVersionTest() {
136    String version = new Inventory(mockProvider.getUrl()).getVersion();
137    assertEquals("Expected version does not match",
138      "{\"system.properties.version\":1.1}", version);
139  }
140
141  @Test
142  @PactVerification(value = "System", fragment = "createPactInvalid")
143  public void runInvalidTest() {
144    String invalid = new Inventory(mockProvider.getUrl()).getInvalidProperty();
145    assertEquals("Expected invalid property response does not match",
146      "", invalid);
147  }
148}

inventory/pom.xml

  1<?xml version='1.0' encoding='utf-8'?>
  2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3
  4    <modelVersion>4.0.0</modelVersion>
  5
  6    <groupId>io.openliberty.guides</groupId>
  7    <artifactId>inventory</artifactId>
  8    <version>1.0-SNAPSHOT</version>
  9    <packaging>war</packaging>
 10
 11    <properties>
 12        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 13        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 14        <maven.compiler.source>1.8</maven.compiler.source>
 15        <maven.compiler.target>1.8</maven.compiler.target>
 16        <!-- Liberty configuration -->
 17        <liberty.var.default.http.port>9081</liberty.var.default.http.port>
 18        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
 19    </properties>
 20
 21    <dependencies>
 22        <dependency>
 23            <groupId>org.eclipse.microprofile</groupId>
 24            <artifactId>microprofile</artifactId>
 25            <version>3.3</version>
 26            <type>pom</type>
 27            <scope>provided</scope>
 28        </dependency>
 29        <dependency>
 30            <groupId>jakarta.platform</groupId>
 31            <artifactId>jakarta.jakartaee-api</artifactId>
 32            <version>8.0.0</version>
 33            <scope>provided</scope>
 34        </dependency>
 35        <!-- For tests -->
 36        <!-- tag::pactJunit[] -->
 37        <dependency>
 38            <groupId>au.com.dius</groupId>
 39            <artifactId>pact-jvm-consumer-junit</artifactId>
 40            <version>4.0.10</version>
 41        </dependency>
 42        <!-- end::pactJunit[] -->
 43        <dependency>
 44            <groupId>org.slf4j</groupId>
 45            <artifactId>slf4j-simple</artifactId>
 46            <version>1.7.30</version>
 47        </dependency>
 48        <dependency>
 49            <groupId>org.apache.cxf</groupId>
 50            <artifactId>cxf-rt-rs-client</artifactId>
 51            <version>3.3.6</version>
 52            <scope>test</scope>
 53        </dependency>
 54    </dependencies>
 55
 56    <build>
 57        <finalName>${project.artifactId}</finalName>
 58        <plugins>
 59            <!-- tag::pactPlugin[] -->
 60            <plugin>
 61                <groupId>au.com.dius.pact.provider</groupId>
 62                <artifactId>maven</artifactId>
 63                <version>4.1.0</version>
 64                <configuration>
 65                    <serviceProviders>
 66                        // tag::serviceProvider[]
 67                        <serviceProvider>
 68                            <name>System</name>
 69                            <protocol>http</protocol>
 70                            <host>localhost</host>
 71                            <port>9080</port>
 72                            <path>/</path>
 73                            // tag::pactDirectory[]
 74                            <pactFileDirectory>target/pacts</pactFileDirectory>
 75                            // end::pactDirectory[]
 76                        </serviceProvider>
 77                        // end::serviceProvider[]
 78                    </serviceProviders>
 79                    <projectVersion>${project.version}</projectVersion>
 80                    <skipPactPublish>false</skipPactPublish>
 81                    <pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
 82                    <tags>
 83                        <tag>open-liberty-pact</tag>
 84                    </tags>
 85                </configuration>
 86            </plugin>
 87            <!-- end::pactPlugin[] -->
 88            <plugin>
 89                <groupId>org.apache.maven.plugins</groupId>
 90                <artifactId>maven-war-plugin</artifactId>
 91                <version>3.2.3</version>
 92            </plugin>
 93            <!-- Plugin to run functional tests -->
 94            <plugin>
 95                <groupId>org.apache.maven.plugins</groupId>
 96                <artifactId>maven-failsafe-plugin</artifactId>
 97                <version>2.22.2</version>
 98                <configuration>
 99                    <systemPropertyVariables>
100                        <http.port>${liberty.var.default.http.port}</http.port>
101                    </systemPropertyVariables>
102                </configuration>
103            </plugin>
104            <!-- Enable liberty-maven plugin -->
105            <plugin>
106                <groupId>io.openliberty.tools</groupId>
107                <artifactId>liberty-maven-plugin</artifactId>
108                <version>3.3</version>
109            </plugin>
110        </plugins>
111    </build>
112</project>
Create the InventoryPactIT class file.
inventory/src/test/java/io/openliberty/guides/inventory/InventoryPactIT.java

The InventoryPactIT class contains a PactProviderRule mock provider that mimics the HTTP responses from the system microservice. The @Pact annotation takes the name of the microservice as a parameter, which makes it easier to differentiate microservices from each other when you have multiple applications.

The createPactServer() method defines the minimal expected response for a specific endpoint, which is known as an interaction. For each interaction, the expected request and the response are registered with the mock service by using the @PactVerification annotation.

The test sends a real request with the getUrl() method of the mock provider. The mock provider compares the actual request with the expected request and confirms whether the comparison is successful. Finally, the assertEquals() method confirms that the response is correct.

Replace the inventory Maven project file.
inventory/pom.xml

The Pact framework provides a Maven plugin that can be added to the build section of the pom.xml file. The serviceProvider element defines the endpoint URL for the system microservice and the pactFileDirectory directory where you want to store the pact file. The pact-jvm-consumer-junit dependency provides the base test class that you can use with JUnit to build unit tests.

After you create the InventoryPactIT.java class and replace the pom.xml file, Open Liberty automatically reloads its configuration.

The contract between the inventory and system microservices is known as a pact. Each pact is a collection of interactions. In this guide, those interactions are defined in the InventoryPactIT class.

Press the enter/return key to run the tests and generate the pact file.

When completed, you’ll see a similar output to the following example:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.openliberty.guides.inventory.InventoryPactIT
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.631 s - in io.openliberty.guides.inventory.InventoryPactIT
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

When you integrate the Pact framework in a CI/CD build pipeline, you can use the mvn failsafe:integration-test goal to generate the pact file. The Maven failsafe plug-in provides a lifecycle phase for running integration tests that run after unit tests. By default, it looks for classes that are suffixed with IT, which stands for Integration Test. You can refer to the Maven failsafe plug-in documentation for more information.

The generated pact file is named Inventory-System.json and is located in the inventory/target/pacts directory. The pact file contains the defined interactions in JSON format:

{
...
"interactions": [
{
      "description": "a request for server name",
      "request": {
        "method": "GET",
        "path": "/system/properties/key/wlp.server.name"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": [
          {
            "wlp.server.name": "defaultServer"
          }
        ]
      },
      "providerStates": [
        {
          "name": "wlp.server.name is defaultServer"
        }
      ]
    }
...
  ]
}

Open a new command-line session and navigate to the start/inventory directory. Publish the generated pact file to the Pact Broker by running the following command:

mvn pact:publish

After the file is published, you’ll see a similar output to the following example:

--- maven:4.1.0:publish (default-cli) @ inventory ---
Publishing 'Inventory-System.json' with tags 'open-liberty-pact' ... OK

Verifying the pact in the Pact Broker

Refresh the Pact Broker webpage at the http://localhost:9292/ URL to verify that a new entry exists. There isn’t yet a timestamp in the last verified column because the pact hasn’t been verified by the system microservice.

Pact Broker webpage for new entry


You can see detailed insights about each interaction by going to the http://localhost:9292/pacts/provider/System/consumer/Inventory/latest URL, as shown in the following image:

Pact Broker webpage for Interactions

Implementing pact testing in the system service

SystemBrokerIT.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2021 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - initial API and implementation
11 *******************************************************************************/
12// end::copyright[]
13package it.io.openliberty.guides.system;
14
15import au.com.dius.pact.provider.junit5.HttpTestTarget;
16import au.com.dius.pact.provider.junit5.PactVerificationContext;
17import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
18import au.com.dius.pact.provider.junitsupport.Consumer;
19import au.com.dius.pact.provider.junitsupport.Provider;
20import au.com.dius.pact.provider.junitsupport.State;
21import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
22import au.com.dius.pact.provider.junitsupport.loader.VersionSelector;
23import org.junit.jupiter.api.BeforeAll;
24import org.junit.jupiter.api.BeforeEach;
25import org.junit.jupiter.api.TestTemplate;
26import org.junit.jupiter.api.extension.ExtendWith;
27
28@Provider("System")
29@Consumer("Inventory")
30// tag::connectionInfo[]
31@PactBroker(
32  host = "localhost",
33  port = "9292",
34  consumerVersionSelectors = {
35    @VersionSelector(tag = "open-liberty-pact")
36  })
37// end::connectionInfo[]
38public class SystemBrokerIT {
39  // tag::invocation[]
40  @TestTemplate
41  @ExtendWith(PactVerificationInvocationContextProvider.class)
42  // tag::context[]
43  void pactVerificationTestTemplate(PactVerificationContext context) {
44    context.verifyInteraction();
45  }
46  // end::context[]
47  // end::invocation[]
48
49  @BeforeAll
50  // tag::publish[]
51  static void enablePublishingPact() {
52    System.setProperty("pact.verifier.publishResults", "true");
53  }
54  // end::publish[]
55
56  @BeforeEach
57  void before(PactVerificationContext context) {
58    int port = Integer.parseInt(System.getProperty("http.port"));
59    context.setTarget(new HttpTestTarget("localhost", port));
60  }
61
62  // tag::state[]
63  @State("wlp.server.name is defaultServer")
64  // end::state[]
65  public void validServerName() {
66  }
67
68  @State("Default directory is true")
69  public void validEdition() {
70  }
71
72  @State("version is 1.1")
73  public void validVersion() {
74  }
75
76  @State("invalid property")
77  public void invalidProperty() {
78  }
79}

system/pom.xml

 1<?xml version='1.0' encoding='utf-8'?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>io.openliberty.guides</groupId>
 8    <artifactId>system</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <packaging>war</packaging>
11
12    <properties>
13        <maven.compiler.source>1.8</maven.compiler.source>
14        <maven.compiler.target>1.8</maven.compiler.target>
15        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
17        <!-- Liberty configuration -->
18        <liberty.var.default.http.port>9080</liberty.var.default.http.port>
19        <liberty.var.default.https.port>9443</liberty.var.default.https.port>
20        <debugPort>8787</debugPort>
21    </properties>
22
23    <dependencies>
24        <!-- Provided dependencies -->
25        <dependency>
26            <groupId>org.eclipse.microprofile</groupId>
27            <artifactId>microprofile</artifactId>
28            <version>3.3</version>
29            <type>pom</type>
30            <scope>provided</scope>
31        </dependency>
32        <dependency>
33            <groupId>jakarta.platform</groupId>
34            <artifactId>jakarta.jakartaee-api</artifactId>
35            <version>8.0.0</version>
36            <scope>provided</scope>
37        </dependency>
38        <!-- tag::pactDependency[] -->
39        <dependency>
40            <groupId>au.com.dius.pact.provider</groupId>
41            <artifactId>junit5</artifactId>
42            <version>4.1.7</version>
43        </dependency>
44        <!-- end::pactDependency[] -->
45        <dependency>
46            <groupId>org.slf4j</groupId>
47            <artifactId>slf4j-simple</artifactId>
48            <version>1.7.30</version>
49        </dependency>
50        <dependency>
51            <groupId>org.apache.cxf</groupId>
52            <artifactId>cxf-rt-rs-client</artifactId>
53            <version>3.3.6</version>
54            <scope>test</scope>
55        </dependency>
56    </dependencies>
57
58    <build>
59        <finalName>${project.artifactId}</finalName>
60        <plugins>
61            <!-- Enable liberty-maven plugin -->
62            <!-- tag::libertyMavenPlugin[] -->
63            <plugin>
64                <groupId>io.openliberty.tools</groupId>
65                <artifactId>liberty-maven-plugin</artifactId>
66                <version>3.3</version>
67            </plugin>
68            <!-- end::libertyMavenPlugin[] -->
69            <plugin>
70                <groupId>org.apache.maven.plugins</groupId>
71                <artifactId>maven-war-plugin</artifactId>
72                <version>3.2.3</version>
73            </plugin>
74            <!-- Plugin to run functional tests -->
75            <plugin>
76                <groupId>org.apache.maven.plugins</groupId>
77                <artifactId>maven-failsafe-plugin</artifactId>
78                <version>3.0.0-M5</version>
79                <configuration>
80                    <systemPropertyVariables>
81                        <http.port>${liberty.var.default.http.port}</http.port>
82                        <!-- tag::version[] -->
83                        <pact.provider.version>${project.version}</pact.provider.version>
84                        <!-- end::version[] -->
85                    </systemPropertyVariables>
86                </configuration>
87            </plugin>
88        </plugins>
89    </build>
90</project>

InventoryPactIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2021 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License v1.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-v10.html
  8 *
  9 * Contributors:
 10 *     IBM Corporation - initial API and implementation
 11 *******************************************************************************/
 12// end::copyright[]
 13
 14package io.openliberty.guides.inventory;
 15
 16import au.com.dius.pact.consumer.dsl.PactDslJsonArray;
 17import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
 18import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
 19import au.com.dius.pact.consumer.junit.PactProviderRule;
 20import au.com.dius.pact.consumer.junit.PactVerification;
 21import au.com.dius.pact.core.model.RequestResponsePact;
 22import au.com.dius.pact.core.model.annotations.Pact;
 23
 24import org.junit.Rule;
 25import org.junit.Test;
 26
 27import static org.junit.Assert.assertEquals;
 28
 29import java.util.HashMap;
 30import java.util.Map;
 31
 32public class InventoryPactIT {
 33  // tag::mockprovider[]
 34  @Rule
 35  public PactProviderRule mockProvider = new PactProviderRule("System", this);
 36  // end::mockprovider[]
 37
 38  // tag::pact[]
 39  @Pact(consumer = "Inventory")
 40  // end::pact[]
 41  // tag::builder[]
 42  public RequestResponsePact createPactServer(PactDslWithProvider builder) {
 43    Map<String, String> headers = new HashMap<String, String>();
 44    headers.put("Content-Type", "application/json");
 45
 46    return builder
 47      // tag::given[]
 48      .given("wlp.server.name is defaultServer")
 49      // end::given[]
 50      .uponReceiving("a request for server name")
 51      .path("/system/properties/key/wlp.server.name")
 52      .method("GET")
 53      .willRespondWith()
 54      .headers(headers)
 55      .status(200)
 56      .body(new PactDslJsonArray().object()
 57        .stringValue("wlp.server.name", "defaultServer"))
 58      .toPact();
 59  }
 60  // end::builder[]
 61
 62  @Pact(consumer = "Inventory")
 63  public RequestResponsePact createPactEdition(PactDslWithProvider builder) {
 64    Map<String, String> headers = new HashMap<String, String>();
 65    headers.put("Content-Type", "application/json");
 66
 67    return builder
 68      .given("Default directory is true")
 69      .uponReceiving("a request to check for the default directory")
 70      .path("/system/properties/key/wlp.user.dir.isDefault")
 71      .method("GET")
 72      .willRespondWith()
 73      .headers(headers)
 74      .status(200)
 75      .body(new PactDslJsonArray().object()
 76        .stringValue("wlp.user.dir.isDefault", "true"))
 77      .toPact();
 78  }
 79
 80  @Pact(consumer = "Inventory")
 81  public RequestResponsePact createPactVersion(PactDslWithProvider builder) {
 82    Map<String, String> headers = new HashMap<String, String>();
 83    headers.put("Content-Type", "application/json");
 84
 85    return builder
 86      .given("version is 1.1")
 87      .uponReceiving("a request for the version")
 88      .path("/system/properties/version")
 89      .method("GET")
 90      .willRespondWith()
 91      .headers(headers)
 92      .status(200)
 93      .body(new PactDslJsonBody()
 94        .decimalType("system.properties.version", 1.1))
 95      .toPact();
 96  }
 97
 98  @Pact(consumer = "Inventory")
 99  public RequestResponsePact createPactInvalid(PactDslWithProvider builder) {
100
101    return builder
102      .given("invalid property")
103      .uponReceiving("a request with an invalid property")
104      .path("/system/properties/invalidProperty")
105      .method("GET")
106      .willRespondWith()
107      .status(404)
108      .toPact();
109  }
110
111  @Test
112  // tag::verification[]
113  @PactVerification(value = "System", fragment = "createPactServer")
114  // end::verification[]
115  public void runServerTest() {
116    // tag::mockTest[]
117    String serverName = new Inventory(mockProvider.getUrl()).getServerName();
118    // end::mockTest[]
119    // tag::unitTest[]
120    assertEquals("Expected server name does not match",
121      "[{\"wlp.server.name\":\"defaultServer\"}]", serverName);
122    // end::unitTest[]
123  }
124
125  @Test
126  @PactVerification(value = "System", fragment = "createPactEdition")
127  public void runEditionTest() {
128    String edition = new Inventory(mockProvider.getUrl()).getEdition();
129    assertEquals("Expected edition does not match",
130      "[{\"wlp.user.dir.isDefault\":\"true\"}]", edition);
131  }
132
133  @Test
134  @PactVerification(value = "System", fragment = "createPactVersion")
135  public void runVersionTest() {
136    String version = new Inventory(mockProvider.getUrl()).getVersion();
137    assertEquals("Expected version does not match",
138      "{\"system.properties.version\":1.1}", version);
139  }
140
141  @Test
142  @PactVerification(value = "System", fragment = "createPactInvalid")
143  public void runInvalidTest() {
144    String invalid = new Inventory(mockProvider.getUrl()).getInvalidProperty();
145    assertEquals("Expected invalid property response does not match",
146      "", invalid);
147  }
148}

Navigate to the start/system directory.

Open another command-line session to start Open Liberty in dev mode for the system microservice:

mvn liberty:dev

After you see the following message, your application server in dev mode is ready:

************************************************************************
*    Liberty is running in dev mode.


Create the SystemBrokerIT class file.
system/src/test/java/it/io/openliberty/guides/system/SystemBrokerIT.java

The connection information for the Pact Broker is provided with the @PactBroker annotation. The dependency also provides a JUnit5 Invocation Context Provider with the pactVerificationTestTemplate() method to generate a test for each of the interactions.

The pact.verifier.publishResults property is set to true so that the results are sent to the Pact Broker after the tests are completed.

The test target is defined in the PactVerificationContext context to point to the running endpoint of the system microservice.

The @State annotation must match the given() parameter that was provided in the inventory test class so that Pact can identify which test case to run against which endpoint.

Replace the system Maven project file.
system/pom.xml

The system microservice uses the junit5 pact provider dependency to connect to the Pact Broker and verify the pact file. Ideally, in a CI/CD build pipeline, the pact.provider.version element is dynamically set to the build number so that it’s easier to identify at which point a breaking change is introduced.

After you create the SystemBrokerIT.java class and replace the pom.xml file, Open Liberty automatically reloads its configuration.

Verifying the contract

In the command-line session where you started the system microservice, press the enter/return key to run the tests to verify the pact file. When you integrate the Pact framework into a CI/CD build pipeline, the mvn failsafe:integration-test goal can be used to verify the pact file from the Pact Broker.

The tests fail with the following errors:

[ERROR] Failures:
[ERROR]   SystemBrokerIT.pactVerificationTestTemplate:44
Failures:

1) Verifying a pact between Inventory and System - a request for the version

    1.1) BodyMismatch: $.0.system.properties.version BodyMismatch: $.0.system.properties.version Expected "1.1" (String) to be a decimal number


[INFO]
[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0

SystemResource.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2021 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License v1.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-v10.html
 8 *
 9 * Contributors:
10 *     IBM Corporation - initial API and implementation
11 *******************************************************************************/
12// end::copyright[]
13package io.openliberty.guides.system;
14
15import javax.json.Json;
16import javax.json.JsonArray;
17import javax.json.JsonObject;
18import javax.ws.rs.PathParam;
19import javax.ws.rs.core.Response;
20
21import javax.enterprise.context.RequestScoped;
22import javax.ws.rs.GET;
23import javax.ws.rs.Path;
24import javax.ws.rs.Produces;
25import javax.ws.rs.core.MediaType;
26
27import org.eclipse.microprofile.metrics.annotation.Counted;
28import org.eclipse.microprofile.metrics.annotation.Timed;
29
30@RequestScoped
31@Path("/properties")
32public class SystemResource {
33
34  @GET
35  @Produces(MediaType.APPLICATION_JSON)
36  @Timed(name = "getPropertiesTime",
37    description = "Time needed to get the JVM system properties")
38  @Counted(absolute = true,
39    description = "Number of times the JVM system properties are requested")
40
41  public Response getProperties() {
42    return Response.ok(System.getProperties()).build();
43  }
44
45  @GET
46  @Path("/key/{key}")
47  @Produces(MediaType.APPLICATION_JSON)
48  public Response getPropertiesByKey(@PathParam("key") String key) {
49    try {
50      JsonArray response = Json.createArrayBuilder()
51        .add(Json.createObjectBuilder()
52          .add(key, System.getProperties().get(key).toString()))
53        .build();
54      return Response.ok(response, MediaType.APPLICATION_JSON).build();
55    } catch (java.lang.NullPointerException exception) {
56        return Response.status(Response.Status.NOT_FOUND).build();
57    }
58  }
59
60  @GET
61  @Path("/version")
62  @Produces(MediaType.APPLICATION_JSON)
63  public JsonObject getVersion() {
64    // tag::decimal[]
65    JsonObject response = Json.createObjectBuilder().add("system.properties.version", 1.1)
66      .build();
67    // end::decimal[]
68    return response;
69  }
70}

The test from the system microservice fails because the inventory microservice was expecting a decimal, 1.1, for the value of the system.properties.version property, but it received a string, "1.1".

Correct the value of the system.properties.version property to a decimal.

Replace the SystemResource class file.
system/src/main/java/io/openliberty/guides/system/SystemResource.java

Press the enter/return key to rerun the tests from the command-line session where you started the system microservice.

If the tests are successful, you’ll see a similar output to the following example:

...
Verifying a pact between pact between Inventory (1.0-SNAPSHOT) and System

  Notices:
    1) The pact at http://localhost:9292/pacts/provider/System/consumer/Inventory/pact-version/XXX is being verified because it matches the following configured selection criterion: latest pact for a consumer version tagged 'open-liberty-pact'

  [from Pact Broker http://localhost:9292/pacts/provider/System/consumer/Inventory/pact-version/XXX]
  Given version is 1.1
  a request for the version
    returns a response which
      has status code 200 (OK)
      has a matching body (OK)
[main] INFO au.com.dius.pact.provider.DefaultVerificationReporter - Published verification result of '[email protected]' for consumer 'Consumer(name=Inventory)'
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.835 s - in it.io.openliberty.guides.system.SystemBrokerIT
...

After the tests are complete, refresh the Pact Broker webpage at the http://localhost:9292/ URL to confirm that there’s now a timestamp in the last verified column:

Pact Broker webpage for verified


The pact file that’s created by the inventory microservice was successfully verified by the system microservice through the Pact Broker. This ensures that responses from the system microservice meet the expectations of the inventory microservice.

Tearing down the environment

When you are done checking out the service, exit dev mode by pressing CTRL+C in the command-line sessions where you ran the servers for the system and inventory microservices, or by typing q and then pressing the enter/return key.

Navigate back to the /guide-contract-testing directory and run the following commands to remove the Pact Broker:

docker-compose -f "pact-broker/docker-compose.yml" down
docker rmi postgres:12
docker rmi pactfoundation/pact-broker:2.62.0.0
docker volume rm pact-broker_postgres-volume

Great work! You’re done!

You implemented contract testing in Java microservices by using Pact and verified the contract with the Pact Broker.

Learn more about the Pact framework.

Guide Attribution

Testing microservices with consumer-driven contracts by Open Liberty is licensed under CC BY-ND 4.0

Copied to clipboard
Copy code block
Copy file contents

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star