In modern distributed systems, microservices rely heavily on inter-service communication. If service contracts break, production failures can cascade across multiple components. Traditional integration testing may not catch contract mismatches early, which is where Contract Testing with Pact and Testcontainers comes in.
This tutorial will guide you through using Pact for consumer-driven contract testing and Testcontainers for running dependent services (databases, message brokers, or mock providers) in isolated, repeatable environments.
What is Contract Testing?
Contract Testing verifies that interactions between a consumer (client) and a provider (service) conform to a shared agreement. Instead of running full-blown end-to-end tests, Pact allows you to:
- Define expected requests and responses in a contract.
- Share that contract with providers.
- Verify both sides independently.
This ensures faster, more reliable tests compared to fragile end-to-end setups.
Why Combine Pact with Testcontainers?
While Pact ensures correctness of contracts, Testcontainers enhances testing by:
- Spinning up real dependencies in Docker.
- Running Pact Broker in a container for contract publishing/sharing.
- Making tests reproducible in CI/CD pipelines.
Together, they provide confidence in both service interactions and infrastructure compatibility.
Setting Up the Project
Maven Dependencies
<dependencies>
<!-- Pact for consumer contract tests -->
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>4.6.9</version>
<scope>test</scope>
</dependency>
<!-- Pact for provider verification -->
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>4.6.9</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers for Pact Broker -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>pact-broker</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Writing a Consumer Contract Test with Pact
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.core.model.RequestResponsePact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(PactConsumerTestExt.class)
public class ConsumerContractTest {
@Pact(consumer = "OrderService", provider = "InventoryService")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("Inventory has stock")
.uponReceiving("A request for item availability")
.path("/inventory/123")
.method("GET")
.willRespondWith()
.status(200)
.body("{\"available\":true}")
.toPact();
}
@Test
void testInventoryAvailability(MockServer mockServer) throws IOException {
URL url = new URL(mockServer.getUrl() + "/inventory/123");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
Scanner scanner = new Scanner(connection.getInputStream());
String response = scanner.useDelimiter("\\A").next();
scanner.close();
assertEquals("{\"available\":true}", response);
}
}
This generates a contract file (OrderService-InventoryService.json
) that can be shared with the provider.
Running a Pact Broker with Testcontainers
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PactBrokerContainer;
import org.testcontainers.utility.DockerImageName;
public class PactBrokerTest {
@Test
void startPactBroker() {
try (PactBrokerContainer pactBroker = new PactBrokerContainer(
DockerImageName.parse("pactfoundation/pact-broker:latest"))) {
pactBroker.start();
System.out.println("Pact Broker running at: " + pactBroker.getFirstMappedPort());
}
}
}
This sets up a disposable Pact Broker for sharing contracts in CI/CD.
Provider Verification with Pact + Testcontainers
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
public class ProviderContractTest {
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerification(PactVerificationContext context) {
context.verifyInteraction();
}
}
This ensures that the provider API implementation matches the consumer’s expectations.
CI/CD Integration
- Jenkins/GitHub Actions can run Pact + Testcontainers in Docker.
- Contracts are published to the Pact Broker container.
- Providers fetch contracts from the broker and verify them.
This ensures fast feedback loops and prevents broken APIs from reaching production.
Best Practices
- Always version contracts to track breaking changes.
- Use Testcontainers for ephemeral Pact Brokers in CI pipelines.
- Automate verification in provider pipelines.
- Combine with Spring Boot + WireMock for stubbing external APIs.
Conclusion + Key Takeaways
- Pact handles consumer-provider contracts.
- Testcontainers ensures reproducible infrastructure for contract sharing.
- Together, they provide fast, reliable feedback in microservice ecosystems.
- Ideal for CI/CD pipelines and cloud-native systems.
FAQ
1. What is the difference between contract and integration testing?
Contract tests validate inter-service agreements, while integration tests validate combined modules.
2. Can Pact work without Testcontainers?
Yes, but Testcontainers makes broker setup easier and CI/CD-friendly.
3. Is Pact only for REST APIs?
No, Pact also supports gRPC, message queues, and GraphQL.
4. Can I use Pact with Spring Boot?
Yes, Pact integrates seamlessly with Spring Boot services.
5. How does Pact Broker help?
It stores and shares contracts between consumers and providers.
6. How to test database interactions?
Use Pact for contracts and Testcontainers for real database verification.
7. What happens if a provider breaks a contract?
The build fails during provider verification, preventing deployment.
8. Can I combine Pact with Testcontainers for Kafka?
Yes, use Pact for message schemas and Testcontainers to run Kafka.
9. Is Pact better than end-to-end tests?
Yes, it’s faster, isolated, and avoids brittle full-stack tests.
10. Should I store contracts in Git?
Yes, versioning contracts in source control ensures traceability.