Contract Testing with Pact & Spring Cloud Contract
Master consumer-driven contract testing to ensure reliable communication between microservices in your E2E test suite.
Understanding Contract Testing
Contract testing verifies that services can communicate with each other by testing the contracts (interfaces) between them without requiring the actual service to be running.
Benefits of Contract Testing
- Early Detection: Catch integration issues before deployment
- Independent Development: Teams can work independently
- Faster Feedback: No need to wait for dependent services
- Reduced Integration Costs: Test contracts, not full integrations
Setting Up Pact for Java
Maven Dependencies
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>java8</artifactId>
<version>4.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>4.6.4</version>
<scope>test</scope>
</dependency>
Consumer Contract Test Example
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService")
public class UserServiceConsumerTest {
@Pact(consumer = "WebApp")
public RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("user exists")
.uponReceiving("get user by id")
.path("/api/users/123")
.method("GET")
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new PactDslJsonBody()
.stringType("id", "123")
.stringType("name", "John Doe")
.stringType("email", "john@example.com"))
.toPact();
}
@Test
@PactTestFor(pactMethod = "createPact")
void testGetUser(MockServer mockServer) {
// Your Selenium test using the mock server
WebDriver driver = new ChromeDriver();
driver.get(mockServer.getUrl() + "/user/123");
// Assert UI reflects correct user data
WebElement userName = driver.findElement(By.id("user-name"));
assertEquals("John Doe", userName.getText());
driver.quit();
}
}
Provider Verification
@Provider("UserService")
@PactBroker(host = "pact-broker.example.com", port = "443", scheme = "https")
public class UserServiceProviderTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@State("user exists")
public void userExists() {
// Setup: Create test user in database
userRepository.save(new User("123", "John Doe", "john@example.com"));
}
@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080));
}
}
Spring Cloud Contract
Contract Definition (Groovy DSL)
Contract.make {
description "should return user by id"
request {
method GET()
url("/api/users/123")
headers {
accept(applicationJson())
}
}
response {
status 200
headers {
contentType(applicationJson())
}
body([
id: "123",
name: "John Doe",
email: "john@example.com"
])
}
}
Auto-Generated Test
Spring Cloud Contract automatically generates tests:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class ContractVerifierTest {
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
}
@Test
public void validate_shouldReturnUserById() throws Exception {
// Auto-generated test code
MockMvcRequestSpecification request = given();
ResponseOptions response = given()
.header("Accept", "application/json")
.get("/api/users/123");
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
}
}
Integration with Selenium Tests
Service Stub Manager
public class ServiceStubManager {
private final Map<String, StubServer> stubs = new HashMap<>();
public void startStub(String serviceName, String contractPath) {
StubServer stub = new StubServer()
.withContracts(contractPath)
.start();
stubs.put(serviceName, stub);
}
public String getServiceUrl(String serviceName) {
return stubs.get(serviceName).getUrl();
}
public void stopAll() {
stubs.values().forEach(StubServer::stop);
}
}
E2E Test with Contract Stubs
@ExtendWith(SeleniumExtension.class)
public class CheckoutE2ETest {
@Autowired
private ServiceStubManager stubManager;
@BeforeEach
void setupStubs() {
stubManager.startStub("PaymentService", "contracts/payment");
stubManager.startStub("InventoryService", "contracts/inventory");
}
@Test
void shouldCompleteCheckoutWithValidPayment() {
WebDriver driver = new ChromeDriver();
// Configure app to use stub services
System.setProperty("payment.service.url",
stubManager.getServiceUrl("PaymentService"));
driver.get("http://localhost:8080/checkout");
// Perform checkout flow
driver.findElement(By.id("card-number")).sendKeys("4111111111111111");
driver.findElement(By.id("submit-payment")).click();
// Verify success
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement success = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.className("success-message")
)
);
assertTrue(success.isDisplayed());
driver.quit();
}
@AfterEach
void tearDown() {
stubManager.stopAll();
}
}
Best Practices
1. Version Your Contracts
@Pact(consumer = "WebApp", provider = "UserService")
@PactVersion("2.0.0")
public RequestResponsePact createPactV2(PactDslWithProvider builder) {
return builder
.given("user v2 exists")
.uponReceiving("get user by id v2")
.path("/api/v2/users/123")
// ... rest of contract
.toPact();
}
2. Use Pact Broker
@PactBroker(
url = "https://pact-broker.company.com",
authentication = @PactBrokerAuth(
username = "${pact.broker.username}",
password = "${pact.broker.password}"
)
)
public class UserServiceProviderTest {
// Provider verification
}
3. Handle Breaking Changes
@State("user with address exists")
public void userWithAddressExists() {
// Support both old and new contract
User user = new User("123", "John Doe");
user.setAddress(new Address("123 Main St")); // New field
userRepository.save(user);
}
Key Takeaways
- Contract testing reduces integration testing overhead
- Pact enables consumer-driven contracts
- Spring Cloud Contract auto-generates tests
- Integrate contract stubs with Selenium E2E tests
- Use Pact Broker for contract versioning and sharing
- Plan for backward compatibility in contracts
Practice Exercise
Create a contract test suite for a microservices-based e-commerce application:
- Define contracts for User, Product, and Order services
- Implement consumer tests with Pact
- Create provider verification tests
- Integrate contract stubs into Selenium E2E tests
- Set up Pact Broker for contract management
- Handle a breaking API change scenario
Next: Learn service virtualization with WireMock and Hoverfly →