Contract Testing in Java Microservices (2025)


Contract Testing in Java Microservices

Contract testing is crucial for ensuring compatibility between microservices in a distributed system. This comprehensive guide explores contract testing tools and techniques in Java microservices.

Pro Tip: Contract testing helps catch integration issues early in the development cycle, reducing deployment risks.

Pact Testing

Note: Pact is a consumer-driven contract testing tool that helps ensure compatibility between service consumers and providers.

Pact Configuration




    au.com.dius.pact.consumer
    junit5
    4.3.2
    test


    au.com.dius.pact.provider
    spring
    4.3.2
    test

Consumer Pact Test


@PactTestFor(providerName = "order-service", hostInterface = "localhost")
public class OrderClientPactTest {
    private OrderClient orderClient;
    
    @BeforeEach
    void setUp() {
        orderClient = new OrderClient("http://localhost:8080");
    }
    
    @Pact(consumer = "order-client")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        return builder
            .given("Order exists")
            .uponReceiving("A request for an order")
            .path("/api/orders/123")
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(Map.of("Content-Type", "application/json"))
            .body(new PactDslJsonBody()
                .numberType("id", 123)
                .stringType("status", "COMPLETED")
                .numberType("amount", 100.0))
            .toPact();
    }
    
    @Test
    @PactTestFor(pactMethod = "createPact")
    void testGetOrder(MockServer mockServer) {
        orderClient.setBaseUrl(mockServer.getUrl());
        Order order = orderClient.getOrder(123);
        
        assertEquals(123, order.getId());
        assertEquals("COMPLETED", order.getStatus());
        assertEquals(100.0, order.getAmount());
    }
}

Spring Cloud Contract

Pro Tip: Spring Cloud Contract provides a DSL for writing contracts and generates tests automatically.

Contract Definition


// src/test/resources/contracts/order-service/order-contract.groovy
package contracts

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "should return order details"
    request {
        method GET()
        url "/api/orders/123"
    }
    response {
        status OK()
        headers {
            contentType applicationJson()
        }
        body([
            id: 123,
            status: "COMPLETED",
            amount: 100.0
        ])
    }
}

Provider Test Generation


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCKMVC)
@AutoConfigureMockMvc
@AutoConfigureStubRunner(
    stubsMode = StubRunnerProperties.StubsMode.LOCAL,
    ids = "com.example:order-service:+:stubs:8080"
)
public class OrderControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void shouldReturnOrderDetails() throws Exception {
        mockMvc.perform(get("/api/orders/123"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(123))
            .andExpect(jsonPath("$.status").value("COMPLETED"))
            .andExpect(jsonPath("$.amount").value(100.0));
    }
}

Consumer-Driven Tests

Note: Consumer-driven contract testing ensures that service providers meet consumer expectations.

Consumer Test Implementation


public class OrderClient {
    private final String baseUrl;
    private final RestTemplate restTemplate;
    
    public OrderClient(String baseUrl) {
        this.baseUrl = baseUrl;
        this.restTemplate = new RestTemplate();
    }
    
    public Order getOrder(long id) {
        String url = baseUrl + "/api/orders/" + id;
        ResponseEntity response = restTemplate.getForEntity(url, Order.class);
        return response.getBody();
    }
}

@SpringBootTest
public class OrderClientIntegrationTest {
    @Autowired
    private OrderClient orderClient;
    
    @Test
    void shouldGetOrderDetails() {
        Order order = orderClient.getOrder(123);
        
        assertNotNull(order);
        assertEquals(123, order.getId());
        assertEquals("COMPLETED", order.getStatus());
        assertEquals(100.0, order.getAmount());
    }
}

Provider Tests

Pro Tip: Provider tests verify that the service implementation matches the contract.

Provider Implementation


@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final OrderService orderService;
    
    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity getOrder(@PathVariable long id) {
        Order order = orderService.getOrder(id);
        return ResponseEntity.ok(order);
    }
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    
    @Autowired
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
    
    public Order getOrder(long id) {
        return orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }
}

Contract Evolution

Note: Managing contract evolution is crucial for maintaining service compatibility.

Contract Versioning


@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 {
    // Version 1 implementation
}

@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 {
    // Version 2 implementation with new fields
}

// Contract for version 1
Contract.make {
    description "should return order details v1"
    request {
        method GET()
        url "/api/v1/orders/123"
    }
    response {
        status OK()
        body([
            id: 123,
            status: "COMPLETED",
            amount: 100.0
        ])
    }
}

// Contract for version 2
Contract.make {
    description "should return order details v2"
    request {
        method GET()
        url "/api/v2/orders/123"
    }
    response {
        status OK()
        body([
            id: 123,
            status: "COMPLETED",
            amount: 100.0,
            customerId: "CUST123",
            orderDate: "2025-03-18T10:00:00Z"
        ])
    }
}

Best Practices

Pro Tip: Following contract testing best practices ensures reliable service integration.

Contract Testing Best Practices

  • Use consumer-driven contract testing
  • Version your contracts
  • Test edge cases and error scenarios
  • Keep contracts simple and focused
  • Use meaningful test data
  • Automate contract testing in CI/CD
  • Document contract changes
  • Review contract changes with stakeholders

Conclusion

Contract testing is essential for maintaining reliable microservices integration. By implementing these techniques and following best practices, teams can ensure service compatibility and reduce integration issues.