Integration Testing
Definition

Database Integration Tests
// Using Testcontainers for real database testing
@SpringBootTest
@Testcontainers
class OrderRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:14")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
}
@Test
void shouldPersistAndRetrieveOrder() {
// Arrange
Order order = new Order("customer-123", Money.of(99.99));
// Act
Order saved = orderRepository.save(order);
Order found = orderRepository.findById(saved.getId()).orElseThrow();
// Assert
assertThat(found.getCustomerId()).isEqualTo("customer-123");
assertThat(found.getTotal()).isEqualTo(Money.of(99.99));
}
@Test
void shouldFindOrdersByDateRange() {
// Arrange
orderRepository.save(new Order("c1", LocalDate.of(2024, 1, 1)));
orderRepository.save(new Order("c2", LocalDate.of(2024, 1, 15)));
orderRepository.save(new Order("c3", LocalDate.of(2024, 2, 1)));
// Act
List<Order> orders = orderRepository.findByDateBetween(
LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 1, 31)
);
// Assert
assertThat(orders).hasSize(2);
}
}
API Integration Tests
// Testing REST endpoints
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class OrderControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
}
@Test
void shouldCreateOrder() {
// Arrange
CreateOrderRequest request = new CreateOrderRequest(
"customer-123",
List.of(new OrderItem("product-1", 2))
);
// Act
ResponseEntity<Order> response = restTemplate.postForEntity(
"/api/orders",
request,
Order.class
);
// Assert
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getCustomerId()).isEqualTo("customer-123");
// Verify persisted
assertThat(orderRepository.count()).isEqualTo(1);
}
@Test
void shouldReturnNotFoundForMissingOrder() {
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/orders/nonexistent-id",
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void shouldReturnBadRequestForInvalidOrder() {
CreateOrderRequest request = new CreateOrderRequest(
null, // Invalid: missing customer
List.of()
);
ResponseEntity<ErrorResponse> response = restTemplate.postForEntity(
"/api/orders",
request,
ErrorResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(response.getBody().getErrors()).contains("customerId is required");
}
}
Message Queue Testing
// Testing message producers and consumers
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = {"orders"})
class OrderEventIntegrationTest {
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderEventConsumer consumer;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldProcessOrderCreatedEvent() throws Exception {
// Arrange
OrderEvent event = new OrderCreatedEvent("order-123", "customer-456");
// Act
kafkaTemplate.send("orders", event).get();
// Wait for async processing
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
// Assert
Optional<Order> order = orderRepository.findById("order-123");
assertThat(order).isPresent();
assertThat(order.get().getStatus()).isEqualTo(Status.PROCESSING);
});
}
}
// Using WireMock for external service mocking
@SpringBootTest
@AutoConfigureWireMock(port = 0)
class PaymentServiceIntegrationTest {
@Autowired
private PaymentService paymentService;
@Test
void shouldHandlePaymentGatewaySuccess() {
// Arrange: Mock external payment API
stubFor(post(urlEqualTo("/payments"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"transactionId\": \"txn-123\", \"status\": \"SUCCESS\"}")
));
// Act
PaymentResult result = paymentService.processPayment(
new PaymentRequest(Money.of(99.99))
);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.getTransactionId()).isEqualTo("txn-123");
}
@Test
void shouldHandlePaymentGatewayTimeout() {
// Arrange: Simulate timeout
stubFor(post(urlEqualTo("/payments"))
.willReturn(aResponse()
.withFixedDelay(5000) // 5 second delay
));
// Act & Assert
assertThrows(PaymentTimeoutException.class, () ->
paymentService.processPayment(new PaymentRequest(Money.of(99.99)))
);
}
}
Test Data Management
// Strategies for managing test data
// 1. Clean before each test
@BeforeEach
void setUp() {
orderRepository.deleteAll();
customerRepository.deleteAll();
}
// 2. Use transactions that rollback
@Transactional
@Test
void shouldCreateOrder() {
// Changes are rolled back after test
}
// 3. Test data builders
class TestDataBuilder {
static Customer aCustomer() {
return Customer.builder()
.id(UUID.randomUUID().toString())
.name("Test Customer")
.email("[email protected]")
.build();
}
static Order anOrder() {
return Order.builder()
.id(UUID.randomUUID().toString())
.customerId("customer-123")
.total(Money.of(99.99))
.build();
}
}
@Test
void shouldProcessOrder() {
Customer customer = TestDataBuilder.aCustomer();
customerRepository.save(customer);
Order order = TestDataBuilder.anOrder()
.customerId(customer.getId());
// ...
}
// 4. Database migrations for test schema
// Flyway/Liquibase with test-specific migrations
// 5. Fixtures for complex data
@Sql("/test-data/orders.sql")
@Test
void shouldCalculateMonthlyRevenue() {
// Test data loaded from SQL file
}
Best Practices

Tips & Tricks
