Skip to content
Home / Skills / Testing / Integration Testing
TE

Integration Testing

Testing core v1.0.0

Integration Testing

Overview

Integration testing verifies that multiple components, modules, or systems work correctly together. Unlike unit tests that isolate individual classes, integration tests validate interactions with real dependencies like databases, message queues, HTTP APIs, and external services. These tests catch issues that unit tests miss: database constraints, transaction boundaries, serialization problems, network failures, and misconfigured integrations.

Modern integration testing leverages TestContainers to run ephemeral Docker containers for dependencies, ensuring tests use production-like infrastructure without complex local setup. Spring Boot Test provides comprehensive support for integration testing with auto-configuration, test slicing, and embedded servers. Well-designed integration tests balance realistic scenarios with reasonable execution time (5-10 seconds per test).

Key Concepts

  • Real Dependencies - Use actual databases, message brokers, and services instead of mocks when testing integration
  • TestContainers - Lightweight, disposable Docker containers for dependencies (PostgreSQL, Redis, Kafka, etc.)
  • Spring Boot Test Slices - Focused test configurations (@WebMvcTest, @DataJpaTest, @RestClientTest)
  • Test Isolation - Each test gets a clean database state; tests don’t affect each other
  • Transaction Rollback - Spring’s @Transactional on test classes rolls back changes after each test
  • Database Migrations - Run Flyway/Liquibase migrations in tests to match production schema
  • API Contract Testing - Verify REST API behavior, status codes, request/response formats
  • Component Boundaries - Test integration points between your code and external systems
  • Performance Expectations - Integration tests should complete in 5-10s; optimize with connection pooling
  • Failure Scenarios - Test error cases: constraint violations, timeouts, connection failures

Best Practices

  1. Use TestContainers for Infrastructure - Run real databases/services in Docker; avoid H2/embedded substitutes
  2. Test Against Production Database - Use the same database engine in tests as production (PostgreSQL, MySQL, etc.)
  3. Run Database Migrations in Tests - Execute Flyway/Liquibase to ensure schema consistency
  4. Isolate Tests with Transactions - Use @Transactional or manual cleanup between tests
  5. Test Database Constraints - Verify unique constraints, foreign keys, and cascading deletes work correctly
  6. Use Realistic Test Data - Create data that resembles production scenarios, not minimal fixtures
  7. Test Error Scenarios - Verify exception handling for constraint violations, timeouts, and connection failures
  8. Limit External API Calls - Mock external services; use WireMock or Mockoon for HTTP dependencies
  9. Parallelize Integration Tests - Configure TestContainers for parallel execution to reduce suite time
  10. Monitor Test Execution Time - Keep integration tests under 10 seconds; optimize slow queries

Code Examples

✅ TestContainers with PostgreSQL

@SpringBootTest
@Testcontainers
class OrderRepositoryIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
        .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;
    
    @Autowired
    private CustomerRepository customerRepository;
    
    @Test
    @Transactional
    void shouldPersistOrderWithItems() {
        // Given
        var customer = customerRepository.save(
            new Customer("john@example.com", "John Doe")
        );
        
        var order = new Order();
        order.setCustomer(customer);
        order.addItem(new OrderItem("PROD-1", 2, 10.0));
        order.addItem(new OrderItem("PROD-2", 1, 25.0));
        
        // When
        var saved = orderRepository.save(order);
        entityManager.flush();
        entityManager.clear();
        
        // Then
        var retrieved = orderRepository.findById(saved.getId());
        assertThat(retrieved).isPresent()
            .hasValueSatisfying(o -> {
                assertThat(o.getCustomer().getEmail()).isEqualTo("john@example.com");
                assertThat(o.getItems()).hasSize(2);
                assertThat(o.getTotalAmount()).isEqualTo(45.0);
            });
    }
    
    @Test
    @Transactional
    void shouldEnforceCustomerForeignKeyConstraint() {
        // Given
        var order = new Order();
        order.setCustomerId(999L); // Non-existent customer
        
        // When/Then
        assertThatThrownBy(() -> {
            orderRepository.save(order);
            entityManager.flush();
        }).isInstanceOf(DataIntegrityViolationException.class)
          .hasMessageContaining("foreign key constraint");
    }
}

✅ Spring Boot Test Slices

// @DataJpaTest - Lightweight JPA repository testing
@DataJpaTest
@Testcontainers
class UserRepositoryTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    
    @DynamicPropertySource
    static void properties(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 UserRepository userRepository;
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Test
    void shouldFindUserByEmail() {
        // Given
        var user = new User("alice@example.com", "Alice");
        entityManager.persist(user);
        entityManager.flush();
        
        // When
        var found = userRepository.findByEmail("alice@example.com");
        
        // Then
        assertThat(found).isPresent()
            .hasValueSatisfying(u -> assertThat(u.getName()).isEqualTo("Alice"));
    }
}

// @WebMvcTest - REST controller testing without full context
@WebMvcTest(OrderController.class)
class OrderControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private OrderService orderService;
    
    @Test
    void shouldCreateOrder() throws Exception {
        // Given
        var request = new CreateOrderRequest("CUST-123", List.of(
            new OrderItemRequest("PROD-1", 2)
        ));
        var order = new Order("ORDER-123", "CUST-123", OrderStatus.PENDING);
        when(orderService.createOrder(any())).thenReturn(order);
        
        // When/Then
        mockMvc.perform(post("/api/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.orderId").value("ORDER-123"))
            .andExpect(jsonPath("$.status").value("PENDING"));
    }
}

✅ Full Integration Test with Multiple Services

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderServiceIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    
    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
    );
    
    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;
    
    @Value("${spring.kafka.topic.order-events}")
    private String orderEventsTopic;
    
    @Test
    void shouldCreateOrderAndPublishEvent() throws Exception {
        // Given
        var request = new CreateOrderRequest("CUST-123", List.of(
            new OrderItemRequest("PROD-1", 2, 10.0)
        ));
        
        var eventCaptor = new ArrayBlockingQueue<OrderEvent>(1);
        var consumer = createKafkaConsumer();
        consumer.subscribe(Collections.singletonList(orderEventsTopic));
        
        // When
        var order = orderService.createOrder(request);
        
        // Then - Order persisted
        var savedOrder = orderRepository.findById(order.getId());
        assertThat(savedOrder).isPresent()
            .hasValueSatisfying(o -> {
                assertThat(o.getStatus()).isEqualTo(OrderStatus.PENDING);
                assertThat(o.getItems()).hasSize(1);
            });
        
        // Then - Event published to Kafka
        var records = consumer.poll(Duration.ofSeconds(5));
        assertThat(records).hasSize(1);
        var event = records.iterator().next().value();
        assertThat(event.getOrderId()).isEqualTo(order.getId());
        assertThat(event.getEventType()).isEqualTo("ORDER_CREATED");
    }
}

✅ Database Migration Testing

@SpringBootTest
@Testcontainers
class FlywayMigrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    
    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        registry.add("spring.flyway.clean-disabled", () -> false);
    }
    
    @Autowired
    private Flyway flyway;
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Test
    void shouldApplyAllMigrations() {
        // When
        flyway.migrate();
        
        // Then
        var info = flyway.info();
        assertThat(info.all()).isNotEmpty();
        assertThat(info.pending()).isEmpty();
        
        // Verify schema structure
        var tables = jdbcTemplate.queryForList(
            "SELECT table_name FROM information_schema.tables " +
            "WHERE table_schema = 'public'",
            String.class
        );
        assertThat(tables).contains("orders", "order_items", "customers");
    }
    
    @Test
    void shouldHandleRepeatablyMigrations() {
        // Clean and re-run migrations
        flyway.clean();
        flyway.migrate();
        
        var firstCount = flyway.info().applied().length;
        
        flyway.migrate(); // Run again
        
        var secondCount = flyway.info().applied().length;
        assertThat(firstCount).isEqualTo(secondCount);
    }
}

❌ Using H2 Instead of Real Database

// ❌ BAD: Testing with H2 when production uses PostgreSQL
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
class OrderRepositoryTest {
    // H2 has different SQL dialect, constraint behavior, and features
    // Tests may pass with H2 but fail in production with PostgreSQL
}

// ✅ GOOD: Use TestContainers with production database
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    // Tests against actual PostgreSQL behavior
}

❌ Mocking Database Interactions

// ❌ BAD: Mocking JPA repository in "integration" test
@SpringBootTest
class OrderServiceTest {
    @MockBean
    private OrderRepository orderRepository;
    
    @Test
    void shouldSaveOrder() {
        when(orderRepository.save(any())).thenReturn(new Order());
        // This is a unit test, not an integration test
        // Doesn't verify actual database interactions
    }
}

// ✅ GOOD: Use real repository with TestContainers
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Test
    void shouldSaveOrder() {
        // Test actual database persistence
        var order = orderService.createOrder(request);
        assertThat(orderRepository.findById(order.getId())).isPresent();
    }
}

Anti-Patterns

  1. Using H2/In-Memory Databases - Testing with different database than production; misses dialect-specific issues
  2. Mocking Everything - Calling it “integration test” but mocking all dependencies; it’s actually a unit test
  3. Shared Test State - Tests affecting each other due to lack of cleanup/transactions
  4. Skipping Database Migrations - Not running Flyway/Liquibase in tests; schema diverges from production
  5. Ignoring Constraints - Not testing unique constraints, foreign keys, or cascading deletes
  6. Manual Docker Setup - Requiring developers to manually start Docker containers instead of using TestContainers
  7. Slow Integration Tests - Tests taking 30+ seconds due to unnecessary full context loads
  8. No Error Scenario Testing - Only testing happy paths; ignoring constraint violations and timeouts

Testing Strategies

TestContainers Singleton Pattern

// Reuse containers across test classes for faster execution
public abstract class AbstractIntegrationTest {
    
    static final PostgreSQLContainer<?> POSTGRES;
    static final KafkaContainer KAFKA;
    
    static {
        POSTGRES = new PostgreSQLContainer<>("postgres:15-alpine")
            .withReuse(true);
        POSTGRES.start();
        
        KAFKA = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"))
            .withReuse(true);
        KAFKA.start();
    }
    
    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
        registry.add("spring.kafka.bootstrap-servers", KAFKA::getBootstrapServers);
    }
}

// Test classes extend the base
@SpringBootTest
class OrderServiceIntegrationTest extends AbstractIntegrationTest {
    // Automatically gets shared containers
}

Test Data Builders

// Create reusable test data builders
class TestDataBuilder {
    
    public static Customer.CustomerBuilder aCustomer() {
        return Customer.builder()
            .email("test@example.com")
            .name("Test Customer")
            .status(CustomerStatus.ACTIVE);
    }
    
    public static Order.OrderBuilder anOrder() {
        return Order.builder()
            .customerId("CUST-123")
            .status(OrderStatus.PENDING)
            .createdAt(Instant.now());
    }
    
    public static OrderItem.OrderItemBuilder anOrderItem() {
        return OrderItem.builder()
            .productId("PROD-1")
            .quantity(1)
            .price(10.0);
    }
}

// Usage in tests
@Test
void shouldProcessOrder() {
    var customer = customerRepository.save(
        aCustomer()
            .email("john@example.com")
            .name("John Doe")
            .build()
    );
    
    var order = orderRepository.save(
        anOrder()
            .customerId(customer.getId())
            .items(List.of(anOrderItem().quantity(2).build()))
            .build()
    );
    
    orderService.processOrder(order.getId());
}

Parallel Test Execution

<!-- Maven Surefire parallel execution -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <parallel>classes</parallel>
        <threadCount>4</threadCount>
        <perCoreThreadCount>true</perCoreThreadCount>
    </configuration>
</plugin>

References