Introduction
When we develop a micro-service system with many REST APIs, we face the problem of testing it. In addition to unit, integration, and end-to-end tests, we must encompass contract testing. Contract testing involves verifying how the interfaces provided by a service satisfy the provider’s requirements. In this article, we will see how to implement contract testing with the Pact software.
A Few Words on Pact
Pact is a tool for performing contract-based testing. It is consumer-driven, which means that it bases the “contract” on the consumer interface. Once the contract is written and stored in a shared place, the producer and consumer can test it independently.
We can use two Junit5-based packages that implement Pact for consumers and producers. A maven plugin is also available to generate the contract in the consumer part. To store the contract we can use a Pact broker. In this case, we can configure the plugin with the host and port of the broker.
Pact Configuration on the Consumer
Suppose we have a Book service with a getAuthor method, that calls an Author service to retrieve some information about an author by its name:
@GetMapping(value = "/findAuthor", params = { "name" })
public Optional<Author> getAuthor(@RequestParam("name") String name) {
return bookService.getAuthor(name);
}
To support Pact we need the following Maven dependencies:
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>4.6.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5spring</artifactId>
<version>4.6.10</version>
<scope>test</scope>
</dependency>
We also add the following, to support Assertj assertions:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
We also add a Maven plugin. It generates the contract in JSON format and stores it in a Pact broker:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<pact.rootDir>target/pacts</pact.rootDir>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-maven_2.11</artifactId>
<version>3.5.24</version>
<configuration>
<pactDirectory>target/pacts</pactDirectory>
<pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
<trimSnapshot>true</trimSnapshot>
</configuration>
</plugin>
We can launch the Pact broker by Docker, using a docker-compose file like the following:
version: '3'
services:
postgres:
image: postgres:13
environment:
POSTGRES_USER: pactbroker
POSTGRES_PASSWORD: password
POSTGRES_DB: pactbroker
volumes:
- postgres_data:/var/lib/postgresql/data
pactbroker:
image: pactfoundation/pact-broker
depends_on:
- postgres
environment:
PACT_BROKER_DATABASE_ADAPTER: postgres
PACT_BROKER_DATABASE_USERNAME: pactbroker
PACT_BROKER_DATABASE_PASSWORD: password
PACT_BROKER_DATABASE_HOST: postgres
PACT_BROKER_DATABASE_NAME: pactbroker
PACT_BROKER_PORT: 9292
ports:
- "9292:9292"
volumes:
postgres_data:
As we can see from the above, the Pact broker uses a Postgres database behind the scenes.
Test implementation
We implement the test for the consumer part by providing the PactConsumerTestExt class to the @ExtendWith annotation. We define a createPact method to create and return the contract. In this case, we define how the findAuthor service behaves. Then we run a test on our contract definition by using the @PactTestFor(pactMethod = “createPact”) annotation:
@ExtendWith(PactConsumerTestExt.class)
public class PactConsumerDrivenContractUnitTest {
@Pact(consumer = "ConsumerService", provider = "ProviderService")
public V4Pact createPact(PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return builder
.given("test GET")
.uponReceiving("GET REQUEST")
.path("/authors/findAuthor")
.query("name=Goethe")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body("{\"name\":\"Goethe\",\"biography\":\"Bio info\"}")
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "createPact")
void runTest(MockServer mockServer) {
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(mockServer.getUrl() + "/authors/findAuthor?name=Goethe", String.class);
assertEquals("{\"name\":\"Goethe\",\"biography\":\"Bio info\"}", response);
}
}
Pact Configuration on the Producer
On the producer side, we set the same dependencies in the pom.xml, but we don’t need the maven plugin to generate and store the contract as on the consumer side. We define a test providing the Pact broker URL and marking the class with @Provider(“ProviderService”):
@Provider("ProviderService")
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@PactBroker(url = "http://localhost:9292")
@AutoConfigureMockMvc
public class ProviderPactTest {
@Autowired
private MockMvc mockMvc;
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void setup(PactVerificationContext context) {
MockMvcTestTarget testTarget = new MockMvcTestTarget(mockMvc);
context.setTarget(testTarget);
}
@State(value = "test GET", action = StateChangeAction.SETUP)
void testGetExists() {
}
}
Conclusion
Pact is a very useful tool to implement contract testing. An implementation of Pact features is available in Spring for the consumer and provider side. You can find the source code of the example of this article on GitHub.
