Testing StrategiesIntermediate
Contract Testing with Pact
Implement consumer-driven contract testing to ensure API compatibility across microservices
4 min read
...
contract-testingpactmicroservicesapi-testing
Introduction
Contract testing ensures that services can communicate effectively without breaking changes. Learn how to implement Pact for consumer-driven contract testing.
What is Contract Testing?
Contract testing verifies that:
- Consumer expects certain API behavior
- Provider delivers that behavior
- Both parties agree on the contract
Problem It Solves
Without contract testing:
Consumer: "I expect field 'userId'"
Provider: "I renamed it to 'user_id'"
Result: 💥 Integration breaksWith contract testing:
Consumer: Defines contract expecting 'userId'
Provider: Tests against contract
Result: ✅ Breaking change detected earlyPact Basics
The Pact Workflow
- Consumer writes tests defining expected interactions
- Pact generates contract file (JSON)
- Contract is shared with provider
- Provider tests against the contract
- Results verified - both sides pass
Consumer Side Testing
Setup (Maven)
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>4.5.0</version>
<scope>test</scope>
</dependency>Writing Consumer Tests
@ExtendWith(PactConsumerTestExt.class)
public class OrderServiceConsumerTest {
@Pact(consumer = "OrderService", provider = "UserService")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("user exists")
.uponReceiving("a request for user details")
.path("/users/123")
.method("GET")
.willRespondWith()
.status(200)
.body(newJsonBody(body -> {
body.stringType("userId", "123");
body.stringType("name", "John Doe");
body.stringType("email", "john@example.com");
}).build())
.toPact();
}
@Test
@PactTestFor(pactMethod = "createPact")
void testGetUser(MockServer mockServer) {
UserClient client = new UserClient(mockServer.getUrl());
User user = client.getUser("123");
assertEquals("123", user.getUserId());
assertEquals("John Doe", user.getName());
}
}Provider Side Testing
Setup
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>4.5.0</version>
<scope>test</scope>
</dependency>Verifying Contracts
@Provider("UserService")
@PactFolder("pacts")
public class UserServiceProviderTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void setUp(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080));
}
@State("user exists")
void userExists() {
// Setup test data
userRepository.save(new User("123", "John Doe", "john@example.com"));
}
}Advanced Scenarios
Request Matching
.body(newJsonBody(body -> {
body.stringType("orderId"); // Must be string
body.numberType("amount"); // Must be number
body.array("items", items -> {
items.object(item -> {
item.stringType("productId");
item.integerType("quantity");
});
});
}).build())Provider States
@State("order exists with id 123")
void orderExists() {
Order order = new Order("123", "CUST1", 100.00);
orderRepository.save(order);
}
@State("order does not exist")
void orderNotFound() {
orderRepository.deleteAll();
}Headers and Authentication
.headers("Authorization", "Bearer token123")
.willRespondWith()
.headers("Content-Type", "application/json")Pact Broker
Why Use Pact Broker?
- Central contract repository
- Version management
- Webhook integration
- Can-I-Deploy verification
Publishing Contracts
// In pom.xml
<plugin>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>maven</artifactId>
<configuration>
<pactBrokerUrl>https://pact-broker.example.com</pactBrokerUrl>
<pactBrokerToken>${env.PACT_BROKER_TOKEN}</pactBrokerToken>
</configuration>
</plugin>mvn pact:publishCI/CD Integration
Consumer Pipeline
# .github/workflows/consumer-tests.yml
- name: Run Pact Tests
run: mvn test
- name: Publish Contracts
run: mvn pact:publish
env:
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}Provider Pipeline
# .github/workflows/provider-verification.yml
- name: Verify Contracts
run: mvn pact:verify
- name: Can I Deploy?
run: |
pact-broker can-i-deploy \
--pacticipant UserService \
--version ${{ github.sha }} \
--to productionBest Practices
-
Test Data Management
- Use provider states effectively
- Clean up between tests
- Use realistic test data
-
Contract Evolution
- Add fields (backward compatible)
- Deprecate before removing
- Version your APIs
-
Matching Rules
- Use type matching, not exact values
- Match arrays properly
- Handle optional fields
-
Organization
- One pact per provider
- Group related interactions
- Name states clearly
Common Pitfalls
Over-Specifying Contracts
❌ Bad:
body.stringValue("email", "john@example.com") // Exact match✅ Good:
body.stringType("email", "john@example.com") // Type matchForgetting Provider States
❌ Bad:
// No @State, test data might not exist✅ Good:
@State("product in stock")
void productInStock() {
productRepository.save(new Product("PROD1", 10));
}Tools and Resources
- Pact Documentation
- Pact Broker
- Pactflow - Managed Pact Broker
Conclusion
Contract testing with Pact enables confident, independent service development. Start with critical service interactions and expand coverage over time.
Part of the QE Hub Testing Strategies series.
Comments (0)
Loading comments...