Skip to content

Integration Testing


Definition

Integration Testing Overview


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

Integration Testing Best Practices


Tips & Tricks

Integration Testing Tips