A Guide to Pact Contract Testing in Spring Boot Applications
- 4.7/5
- 678
- Feb 02, 2025
In this article we will create an OpenAPI contract, develop a Spring Boot API, and write Pact tests to ensure the API adheres to the contract.
Contract tests ensure that the messages exchanged between different services (such as microservices) conform to a shared understanding, documented in the contract, which both the consumer and the provider adhere to.
Without contract tests, teams would rely on integration tests to verify that services can work together, which can become expensive and fragile, especially as the number of services grows.
By utilizing contract tests, services can independently validate whether they meet the required expectations, helping avoid breaking changes and reducing the need for tight coupling and excessive integration testing.
1) Pact reduces the need for complex, end-to-end integration tests, which are typically slower and more expensive to maintain.
2) Pact offers quicker validation through isolated tests for specific interactions, compared to broader integration tests.
3) Pact creates a clear contract between interacting services, ensuring that changes in one service don't unexpectedly break another, even when they evolve independently.
In this setup, two services—one acting as a consumer and the other as a provider—interact via a REST API. The consumer expects to receive a specific status code and response body from the provider.
What is Pact (Contract Testing)?
Pact testing is a code-first approach designed for verifying interactions between applications through contract testing.Contract tests ensure that the messages exchanged between different services (such as microservices) conform to a shared understanding, documented in the contract, which both the consumer and the provider adhere to.
Without contract tests, teams would rely on integration tests to verify that services can work together, which can become expensive and fragile, especially as the number of services grows.
By utilizing contract tests, services can independently validate whether they meet the required expectations, helping avoid breaking changes and reducing the need for tight coupling and excessive integration testing.
1) Pact reduces the need for complex, end-to-end integration tests, which are typically slower and more expensive to maintain.
2) Pact offers quicker validation through isolated tests for specific interactions, compared to broader integration tests.
3) Pact creates a clear contract between interacting services, ensuring that changes in one service don't unexpectedly break another, even when they evolve independently.
How Pact Works?
Pact provides an RSpec-based Domain Specific Language (DSL) for service consumers to define the HTTP requests they will make to a service provider, as well as the HTTP responses they expect to receive in return.In this setup, two services—one acting as a consumer and the other as a provider—interact via a REST API. The consumer expects to receive a specific status code and response body from the provider.
By using Pact as a mock provider, you can avoid directly interacting with the real service during testing. On the consumer side, you write tests that mock the expected data from the provider. These tests cover various scenarios, including the expected status code and response values.
Pact automatically generates a contract file (in JSON format) based on the interactions defined in the consumer tests and uploads it to the Pact Broker.
The provider service retrieves this contract file from the Pact Broker. It then executes the interactions against the actual provider service, comparing the actual results to the expected outcomes defined in the contract.
This contract-based testing approach ensures that any changes made by the provider to the API are immediately flagged during development. If the provider's API changes in a way that breaks the consumer's expectations, the contract test will fail, signaling that local changes may have impacted the consumer service.
For more on Pact and its advantages in contract testing, check out official resources like the Pact.io documentation.
Pact contract testing with Spring BOOT
Consumer (order-service-consumer): This service sends requests to the "inventory-service-provider" to fetch product details. It expects a specific response, including product information and the status code.Provider (inventory-service-provider): This service receives requests from the consumer and returns the product details based on the request. The provider must fulfill the consumer's expectations as defined in the contract.
Consumer's Side
Add the Consumer Pact dependencies and the Pact Broker configuration in the pom.xml.<dependency> <groupId>au.com.dius.pact.consumer</groupId> <artifactId>junit5</artifactId> <version>4.6.16</version> </dependency>
<plugin> <groupId>au.com.dius.pact.provider</groupId> <artifactId>maven</artifactId> <version>4.6.16</version> <configuration> <pactBrokerUrl>https://org-cb.pactflow.io</pactBrokerUrl> <pactBrokerToken>k-XpYTGvrMFNJaKsCZLRWe</pactBrokerToken> </configuration> </plugin>
PactConsumerTest
It defines a consumer-side test that ensures the consumer's expectations align with the provider's API behavior.1) Add Pact configurations at the Consumer class:
@SpringBootTest: This annotation is part of the Spring Boot testing framework and is used to indicate that the class should be treated as a Spring Boot test. It ensures the Spring context is loaded for the test.@ExtendWith(PactConsumerTestExt.class): This JUnit 5 annotation is used to register extensions in the test class. It integrates Pact’s features for contract testing in the consumer side of the application.
@PactTestFor: This annotation is used to specify the provider against which the interactions defined in the test should be verified. The provider's name, in this case, InventoryProvider, should match exactly with the name defined in the provider. This binds the consumer and provider, establishing the contract to be validated during testing.
2) Prepare a mock response for what the Provider (inventory-service-provider) is supposed to respond with:
In Pact testing, you simulate the response from the provider (cinventory-service-provider) that the consumer (client) expects. This mock response will be used to verify if the consumer can correctly handle the response and adhere to the contract.@Pact annotation: This annotation is used to mark the method that defines the Pact contract between the consumer and the provider. It specifies the interactions, including HTTP request details (method, path, headers, body) and the expected response (status, body). This creates the pact document (usually in JSON format), which serves as the basis for verification during the provider-side contract test.
3) Write the test, making a POST request with the resource, and write assertions for the status code and response:
The test will make a POST request to the mock server, using the mock response that was defined earlier. After sending the request, assertions are written to verify that the response status code and body match the expected values as defined in the contract.@PactTestFor annotation: This annotation indicates that this test is associated with the interactions defined previously in the contract (or mock response).
MockServer object: This object is automatically injected by Pact and provides the URL where the mock server is running. The mock server simulates the provider's behavior, so the consumer can be tested against the expected interactions without actually making a real network request to the provider.
Upload the contract file to the Pact Broker
When the tests are executed, Pact generates a contract file in the form of a JSON document, which contains the expected interactions between the consumer and provider.This file is stored in the target/pacts directory of the project. It represents the agreed-upon expectations for the interactions, such as the HTTP request method, path, body, and the expected response.
Once the contract file is created during the test execution, it needs to be uploaded to a Pact Broker for further verification by the provider. The Pact Broker is a cloud service that stores the contracts and allows both the consumer and provider to share and manage these interactions.
To upload the contract file to the Pact Broker, you can do this either manually or automate the process using a build tool like Maven. To automate the process with Maven, the following goal can be used:
mvn pact:publish
Provider's Side
To add the provider-side Pact dependencies in your pom.xml, you need the following dependency for JUnit 5 support:<dependency> <groupId>au.com.dius.pact.provider</groupId> <artifactId>junit5</artifactId> <version>4.6.16</version> </dependency>
PactProviderTest
This code demonstrates how to configure and run a Pact provider test using JUnit 5, validating the implementation of a service (InventoryProvider) against the contract defined in a Pact file.Adding Pact Configurations to the Provider Class
@Provider Annotation: This annotation specifies the name of the provider service being tested. The name provided here should match the one defined in the consumer's @PactTestFor annotation to ensure consistency and proper contract verification.@PactBroker Annotation: This annotation is used to specify the details of the Pact Broker. It facilitates downloading the contract files for verification by the provider. You can include the account URL and authentication token for secure access. If the contract files are stored locally, use @PactFolder annotation to specify the folder containing the contract JSON files.
@TestTemplate is part of the JUnit framework that allows defining a reusable template for generating multiple tests.
PactVerificationInvocationContextProvider ensures a test is generated for each interaction found in the Pact files.
Write the test to run the interactions from the contract file
The @BeforeEach method initializes the PactVerificationContext with server details where the provider service is running. HttpTestTarget is used to specify the host and port of the provider.@State annotations define the preconditions required by the interactions in the contract. The state name provided in the @State annotation in the provider must match the state defined in the consumer's mock interactions.
Run the provider tests
Executing the provider tests validates the interactions defined in the contract file by comparing the actual responses from the provider service against the expected results specified in the contract file. This ensures that the provider adheres to the agreed-upon contract.When running provider tests, you should observe logs that indicate the following stages and information:
Verifying a pact between OrderConsumer (0.0.1-SNAPSHOT) and InventoryProvider Notices: 1) The pact at https://org-cb.pactflow.io/pacts/provider/InventoryProvider/consumer/OrderConsumer/pact-version/4b0e3025fdd4a9f5ac7b0be21fe278fd7b7ad025 is being verified because the pact content belongs to the consumer version matching the following criterion: * latest version of OrderConsumer that has a pact with InventoryProvider (0.0.1-SNAPSHOT) [from Pact Broker https://org-cb.pactflow.io/pacts/provider/InventoryProvider/consumer/OrderConsumer/pact-version/4b0e3025fdd4a9f5ac7b0be21fe278fd7b7ad025/metadata/c1tdW2xdPXRydWUmc1tdW2N2XT05] Given Inventory exists A request for inventory 2025-01-26T18:18:31.655+05:30 WARN 10891 --- [inventory-service-provider] [ Thread-3] au.com.dius.pact.core.support.Metrics : Please note: we are tracking events anonymously to gather important usage statistics like JVM version and operating system. To disable tracking, set the 'pact_do_not_track' system property or environment variable to 'true'. returns a response which has status code 200 (OK) has a matching body (OK) 2025-01-26T18:18:31.678+05:30 WARN 10891 --- [inventory-service-provider] [ main] a.c.dius.pact.provider.ProviderVersion : Provider version not set, defaulting to '0.0.0' 2025-01-26T18:18:32.033+05:30 INFO 10891 --- [inventory-service-provider] [ main] a.c.d.p.p.DefaultVerificationReporter : Published verification result of 'Ok(interactionIds=[d9e74d18d426b2bab4a924893566679d9771b7bb])' for consumer 'Consumer(name=OrderConsumer)'And the following data appears in the PactFlow dashboard as shown in the image below:
Source Code: GitHub