Lesson 6 of 14 ~90 min
Course progress
0%

Contract Testing with Pact & Spring Cloud Contract

Implement consumer-driven contract testing for microservices using Pact and Spring Cloud Contract

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:

  1. Define contracts for User, Product, and Order services
  2. Implement consumer tests with Pact
  3. Create provider verification tests
  4. Integrate contract stubs into Selenium E2E tests
  5. Set up Pact Broker for contract management
  6. Handle a breaking API change scenario

Next: Learn service virtualization with WireMock and Hoverfly →