1. java
  2. /testing
  3. /integration-testing

Integration Testing in Java Applications

Integration Testing in Java

Integration testing verifies that different components of an application work correctly together. Unlike unit tests that test individual components in isolation, integration tests validate the interactions between modules, external services, databases, and other system components.

Table of Contents

Integration Testing Fundamentals

Types of Integration Testing

Big Bang Integration

  • All components are integrated simultaneously
  • Testing happens after all modules are developed
  • Difficult to isolate defects

Incremental Integration

  • Components are integrated one by one
  • Testing happens progressively
  • Easier to identify and fix issues

Top-Down Integration

  • Testing starts from top-level modules
  • Uses stubs for lower-level modules

Bottom-Up Integration

  • Testing starts from lowest-level modules
  • Uses drivers for higher-level modules

Test Strategy

// Integration test categorization
@Tag("integration")
public class OrderServiceIntegrationTest {
    
    // Test component interactions within the application
    @Test
    void shouldProcessOrderWithInventoryCheck() {
        // Tests OrderService + InventoryService + PaymentService
    }
    
    // Test external system integrations
    @Test
    void shouldSendEmailNotificationAfterOrderComplete() {
        // Tests OrderService + EmailService + External Email Provider
    }
    
    // Test data persistence interactions
    @Test
    void shouldPersistOrderAndUpdateInventory() {
        // Tests OrderService + Database + Transaction Management
    }
}

Spring Boot Integration Testing

@SpringBootTest Overview

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

// Full application context loading
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class FullApplicationIntegrationTest {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void shouldProcessOrderEndToEnd() {
        // Test with full application context
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(1L);
        request.addItem(new OrderItem("product1", 2, BigDecimal.valueOf(29.99)));
        
        Order order = orderService.createOrder(request);
        
        assertThat(order)
            .isNotNull()
            .satisfies(o -> {
                assertThat(o.getId()).isNotNull();
                assertThat(o.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
                assertThat(o.getTotalAmount()).isEqualTo(BigDecimal.valueOf(59.98));
            });
    }
}

// Web environment configuration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void shouldCreateOrderViaRestAPI() {
        String url = "http://localhost:" + port + "/api/orders";
        
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(1L);
        
        ResponseEntity<Order> response = restTemplate.postForEntity(
            url, request, Order.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
    }
}

Test Slices

// Web layer testing
@WebMvcTest(OrderController.class)
class OrderControllerIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private OrderService orderService;
    
    @Test
    void shouldCreateOrderThroughController() throws Exception {
        Order expectedOrder = new Order();
        expectedOrder.setId(1L);
        expectedOrder.setStatus(OrderStatus.CONFIRMED);
        
        when(orderService.createOrder(any())).thenReturn(expectedOrder);
        
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"customerId\":1,\"items\":[]}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.status").value("CONFIRMED"));
    }
}

// JPA repository testing
@DataJpaTest
class OrderRepositoryIntegrationTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Test
    void shouldFindOrdersByCustomerId() {
        // Given
        Customer customer = new Customer("John Doe", "[email protected]");
        entityManager.persistAndFlush(customer);
        
        Order order1 = new Order();
        order1.setCustomer(customer);
        order1.setStatus(OrderStatus.CONFIRMED);
        entityManager.persistAndFlush(order1);
        
        Order order2 = new Order();
        order2.setCustomer(customer);
        order2.setStatus(OrderStatus.SHIPPED);
        entityManager.persistAndFlush(order2);
        
        // When
        List<Order> orders = orderRepository.findByCustomerId(customer.getId());
        
        // Then
        assertThat(orders)
            .hasSize(2)
            .extracting(Order::getStatus)
            .containsExactlyInAnyOrder(OrderStatus.CONFIRMED, OrderStatus.SHIPPED);
    }
}

// JSON serialization testing
@JsonTest
class OrderJsonTest {
    
    @Autowired
    private JacksonTester<Order> json;
    
    @Test
    void shouldSerializeOrder() throws Exception {
        Order order = new Order();
        order.setId(1L);
        order.setStatus(OrderStatus.CONFIRMED);
        order.setTotalAmount(BigDecimal.valueOf(99.99));
        
        assertThat(json.write(order))
            .extractingJsonPathNumberValue("$.id").isEqualTo(1)
            .extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED")
            .extractingJsonPathNumberValue("$.totalAmount").isEqualTo(99.99);
    }
    
    @Test
    void shouldDeserializeOrder() throws Exception {
        String content = """
            {
                "id": 1,
                "status": "CONFIRMED",
                "totalAmount": 99.99
            }
            """;
        
        Order order = json.parseObject(content);
        
        assertThat(order.getId()).isEqualTo(1L);
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
        assertThat(order.getTotalAmount()).isEqualTo(BigDecimal.valueOf(99.99));
    }
}

Database Integration Testing

In-Memory Database Testing

@SpringBootTest
@Transactional
@Rollback
class DatabaseIntegrationTest {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private CustomerRepository customerRepository;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    void shouldCreateOrderWithDatabasePersistence() {
        // Setup test data
        Customer customer = new Customer("John Doe", "[email protected]");
        customer = customerRepository.save(customer);
        
        Product product = new Product("Laptop", BigDecimal.valueOf(999.99));
        product = productRepository.save(product);
        
        // Create order
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(customer.getId());
        request.addItem(new OrderItem(product.getId(), 1, product.getPrice()));
        
        Order order = orderService.createOrder(request);
        
        // Verify persistence
        assertThat(order.getId()).isNotNull();
        
        Order savedOrder = orderService.findById(order.getId());
        assertThat(savedOrder)
            .isNotNull()
            .satisfies(o -> {
                assertThat(o.getCustomer().getId()).isEqualTo(customer.getId());
                assertThat(o.getItems()).hasSize(1);
                assertThat(o.getTotalAmount()).isEqualTo(BigDecimal.valueOf(999.99));
            });
    }
    
    @Test
    void shouldHandleTransactionalRollback() {
        Customer customer = customerRepository.save(new Customer("Jane Doe", "[email protected]"));
        
        // This should trigger a rollback
        assertThrows(IllegalArgumentException.class, () -> {
            orderService.createInvalidOrder(customer.getId());
        });
        
        // Verify rollback occurred
        List<Order> orders = orderService.findOrdersByCustomerId(customer.getId());
        assertThat(orders).isEmpty();
    }
}

Custom Database Configuration

@TestConfiguration
public class TestDatabaseConfig {
    
    @Bean
    @Primary
    @Profile("test")
    public DataSource testDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
        config.setUsername("sa");
        config.setPassword("");
        config.setDriverClassName("org.h2.Driver");
        return new HikariDataSource(config);
    }
    
    @Bean
    @Profile("test")
    public PlatformTransactionManager testTransactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

// SQL script execution
@SpringBootTest
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class SqlScriptIntegrationTest {
    
    @Autowired
    private CustomerRepository customerRepository;
    
    @Test
    void shouldUseTestDataFromSqlScript() {
        List<Customer> customers = customerRepository.findAll();
        assertThat(customers).hasSizeGreaterThan(0);
        
        Customer customer = customerRepository.findByEmail("[email protected]");
        assertThat(customer).isNotNull();
    }
}

Database Migration Testing

@SpringBootTest
@TestPropertySource(properties = {
    "spring.flyway.locations=classpath:db/migration,classpath:db/testdata"
})
class DatabaseMigrationTest {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Test
    void shouldApplyMigrationsCorrectly() {
        // Test that migrations have been applied
        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM flyway_schema_history", Integer.class);
        
        assertThat(count).isGreaterThan(0);
        
        // Test that tables were created
        Boolean customersTableExists = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) > 0 FROM information_schema.tables WHERE table_name = 'CUSTOMERS'",
            Boolean.class);
        
        assertThat(customersTableExists).isTrue();
    }
    
    @Test
    void shouldHaveCorrectTableStructure() {
        // Test table structure
        List<Map<String, Object>> columns = jdbcTemplate.queryForList(
            "SELECT column_name, data_type FROM information_schema.columns " +
            "WHERE table_name = 'CUSTOMERS' ORDER BY ordinal_position");
        
        assertThat(columns)
            .hasSize(4)
            .extracting(col -> col.get("COLUMN_NAME"))
            .containsExactly("ID", "FIRST_NAME", "LAST_NAME", "EMAIL");
    }
}

Web Layer Testing

REST API Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(OrderAnnotation.class)
class OrderApiIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @LocalServerPort
    private int port;
    
    private String baseUrl;
    
    @BeforeEach
    void setUp() {
        baseUrl = "http://localhost:" + port + "/api";
    }
    
    @Test
    @Order(1)
    void shouldCreateCustomer() {
        CreateCustomerRequest request = new CreateCustomerRequest();
        request.setFirstName("John");
        request.setLastName("Doe");
        request.setEmail("[email protected]");
        
        ResponseEntity<Customer> response = restTemplate.postForEntity(
            baseUrl + "/customers", request, Customer.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody())
            .isNotNull()
            .satisfies(customer -> {
                assertThat(customer.getId()).isNotNull();
                assertThat(customer.getEmail()).isEqualTo("[email protected]");
            });
    }
    
    @Test
    @Order(2)
    void shouldCreateOrder() {
        // First create a customer
        Customer customer = createTestCustomer();
        
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(customer.getId());
        request.addItem(new OrderItemRequest("product1", 2, BigDecimal.valueOf(25.00)));
        
        ResponseEntity<Order> response = restTemplate.postForEntity(
            baseUrl + "/orders", request, Order.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody())
            .isNotNull()
            .satisfies(order -> {
                assertThat(order.getId()).isNotNull();
                assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
                assertThat(order.getTotalAmount()).isEqualTo(BigDecimal.valueOf(50.00));
            });
    }
    
    @Test
    @Order(3)
    void shouldGetOrderById() {
        Customer customer = createTestCustomer();
        Order order = createTestOrder(customer.getId());
        
        ResponseEntity<Order> response = restTemplate.getForEntity(
            baseUrl + "/orders/" + order.getId(), Order.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody())
            .isNotNull()
            .satisfies(o -> {
                assertThat(o.getId()).isEqualTo(order.getId());
                assertThat(o.getCustomer().getId()).isEqualTo(customer.getId());
            });
    }
    
    @Test
    void shouldReturnNotFoundForNonExistentOrder() {
        ResponseEntity<String> response = restTemplate.getForEntity(
            baseUrl + "/orders/999999", String.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }
    
    @Test
    void shouldValidateOrderCreationRequest() {
        CreateOrderRequest invalidRequest = new CreateOrderRequest();
        // Missing required fields
        
        ResponseEntity<ValidationErrorResponse> response = restTemplate.postForEntity(
            baseUrl + "/orders", invalidRequest, ValidationErrorResponse.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(response.getBody())
            .isNotNull()
            .satisfies(error -> {
                assertThat(error.getErrors()).isNotEmpty();
                assertThat(error.getErrors()).containsKey("customerId");
            });
    }
    
    private Customer createTestCustomer() {
        CreateCustomerRequest request = new CreateCustomerRequest();
        request.setFirstName("Test");
        request.setLastName("Customer");
        request.setEmail("test" + System.currentTimeMillis() + "@example.com");
        
        return restTemplate.postForEntity(baseUrl + "/customers", request, Customer.class)
                          .getBody();
    }
    
    private Order createTestOrder(Long customerId) {
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(customerId);
        request.addItem(new OrderItemRequest("test-product", 1, BigDecimal.valueOf(10.00)));
        
        return restTemplate.postForEntity(baseUrl + "/orders", request, Order.class)
                          .getBody();
    }
}

MockMvc Integration Testing

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderControllerMockMvcTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    @Transactional
    void shouldCreateOrderWithValidRequest() throws Exception {
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(1L);
        request.addItem(new OrderItemRequest("product1", 2, BigDecimal.valueOf(25.00)));
        
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").exists())
                .andExpect(jsonPath("$.status").value("PENDING"))
                .andExpect(jsonPath("$.totalAmount").value(50.00))
                .andExpect(jsonPath("$.items").isArray())
                .andExpect(jsonPath("$.items", hasSize(1)));
    }
    
    @Test
    void shouldReturnValidationErrorForInvalidRequest() throws Exception {
        CreateOrderRequest invalidRequest = new CreateOrderRequest();
        // Missing required fields
        
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidRequest)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errors").exists())
                .andExpect(jsonPath("$.errors.customerId").exists());
    }
    
    @Test
    void shouldGetOrderWithAllAssociations() throws Exception {
        mockMvc.perform(get("/api/orders/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.customer").exists())
                .andExpect(jsonPath("$.customer.id").exists())
                .andExpect(jsonPath("$.items").isArray())
                .andExpect(jsonPath("$.items[0].product").exists());
    }
}

External Service Testing

WireMock for External APIs

@SpringBootTest
class ExternalServiceIntegrationTest {
    
    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().port(8089))
        .build();
    
    @Autowired
    private PaymentService paymentService;
    
    @Test
    void shouldProcessPaymentSuccessfully() {
        // Setup WireMock stub
        wireMock.stubFor(post(urlEqualTo("/api/payments"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "transactionId": "txn_12345",
                        "status": "SUCCESS",
                        "amount": 100.00
                    }
                    """)));
        
        PaymentRequest request = new PaymentRequest();
        request.setAmount(BigDecimal.valueOf(100.00));
        request.setCardNumber("4111111111111111");
        request.setExpiryMonth("12");
        request.setExpiryYear("2025");
        
        PaymentResult result = paymentService.processPayment(request);
        
        assertThat(result)
            .isNotNull()
            .satisfies(r -> {
                assertThat(r.getTransactionId()).isEqualTo("txn_12345");
                assertThat(r.getStatus()).isEqualTo(PaymentStatus.SUCCESS);
                assertThat(r.getAmount()).isEqualTo(BigDecimal.valueOf(100.00));
            });
        
        // Verify the request was made
        wireMock.verify(postRequestedFor(urlEqualTo("/api/payments"))
            .withHeader("Content-Type", equalTo("application/json"))
            .withRequestBody(matchingJsonPath("$.amount", equalTo("100.0"))));
    }
    
    @Test
    void shouldHandlePaymentFailure() {
        wireMock.stubFor(post(urlEqualTo("/api/payments"))
            .willReturn(aResponse()
                .withStatus(400)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "error": "INVALID_CARD",
                        "message": "Invalid card number"
                    }
                    """)));
        
        PaymentRequest request = new PaymentRequest();
        request.setAmount(BigDecimal.valueOf(100.00));
        request.setCardNumber("4000000000000002");
        
        assertThrows(PaymentException.class, () -> {
            paymentService.processPayment(request);
        });
    }
    
    @Test
    void shouldRetryOnNetworkFailure() {
        // First call fails, second succeeds
        wireMock.stubFor(post(urlEqualTo("/api/payments"))
            .inScenario("retry-scenario")
            .whenScenarioStateIs(Scenario.STARTED)
            .willReturn(aResponse()
                .withStatus(500)
                .withFixedDelay(1000))
            .willSetStateTo("failed-once"));
        
        wireMock.stubFor(post(urlEqualTo("/api/payments"))
            .inScenario("retry-scenario")
            .whenScenarioStateIs("failed-once")
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("""
                    {
                        "transactionId": "txn_retry_123",
                        "status": "SUCCESS",
                        "amount": 100.00
                    }
                    """)));
        
        PaymentRequest request = new PaymentRequest();
        request.setAmount(BigDecimal.valueOf(100.00));
        request.setCardNumber("4111111111111111");
        
        PaymentResult result = paymentService.processPayment(request);
        
        assertThat(result.getStatus()).isEqualTo(PaymentStatus.SUCCESS);
        
        // Verify retry happened
        wireMock.verify(2, postRequestedFor(urlEqualTo("/api/payments")));
    }
}

Email Service Integration Testing

@SpringBootTest
class EmailServiceIntegrationTest {
    
    @Autowired
    private EmailService emailService;
    
    @MockBean
    private JavaMailSender mailSender;
    
    @Captor
    private ArgumentCaptor<MimeMessage> messageCaptor;
    
    @Test
    void shouldSendOrderConfirmationEmail() throws Exception {
        Order order = createTestOrder();
        
        emailService.sendOrderConfirmation(order);
        
        verify(mailSender).send(messageCaptor.capture());
        
        MimeMessage sentMessage = messageCaptor.getValue();
        assertThat(sentMessage.getSubject()).isEqualTo("Order Confirmation - #" + order.getId());
        assertThat(sentMessage.getAllRecipients())
            .hasSize(1)
            .extracting(Address::toString)
            .contains(order.getCustomer().getEmail());
        
        // Verify email content
        String content = sentMessage.getContent().toString();
        assertThat(content).contains("Order #" + order.getId());
        assertThat(content).contains(order.getCustomer().getFirstName());
        assertThat(content).contains(order.getTotalAmount().toString());
    }
    
    @Test
    void shouldHandleEmailSendingFailure() {
        Order order = createTestOrder();
        
        doThrow(new MailException("SMTP server unavailable") {})
            .when(mailSender).send(any(MimeMessage.class));
        
        // Should not throw exception, should log error and continue
        assertDoesNotThrow(() -> {
            emailService.sendOrderConfirmation(order);
        });
        
        verify(mailSender).send(any(MimeMessage.class));
    }
    
    private Order createTestOrder() {
        Customer customer = new Customer();
        customer.setId(1L);
        customer.setFirstName("John");
        customer.setLastName("Doe");
        customer.setEmail("[email protected]");
        
        Order order = new Order();
        order.setId(123L);
        order.setCustomer(customer);
        order.setTotalAmount(BigDecimal.valueOf(99.99));
        order.setStatus(OrderStatus.CONFIRMED);
        
        return order;
    }
}

Test Containers

Database Test Containers

@SpringBootTest
@Testcontainers
class PostgreSQLIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .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;
    
    @Test
    void shouldPersistOrderInPostgreSQL() {
        Order order = new Order();
        order.setStatus(OrderStatus.PENDING);
        order.setTotalAmount(BigDecimal.valueOf(99.99));
        
        Order savedOrder = orderRepository.save(order);
        
        assertThat(savedOrder.getId()).isNotNull();
        
        Optional<Order> foundOrder = orderRepository.findById(savedOrder.getId());
        assertThat(foundOrder).isPresent();
    }
}

// Multiple containers
@SpringBootTest
@Testcontainers
class MultiContainerIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
    
    @Container
    static RedisContainer redis = new RedisContainer("redis:6");
    
    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"));
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        // Database
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
        
        // Redis
        registry.add("spring.redis.host", redis::getHost);
        registry.add("spring.redis.port", redis::getFirstMappedPort);
        
        // Kafka
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private CacheManager cacheManager;
    
    @KafkaListener(topics = "order-events")
    public void handleOrderEvent(OrderEvent event) {
        // Test Kafka integration
    }
    
    @Test
    void shouldIntegrateWithAllServices() {
        // Test database, cache, and messaging integration
        Order order = orderService.createOrder(new CreateOrderRequest());
        
        // Verify database persistence
        assertThat(order.getId()).isNotNull();
        
        // Verify caching
        Order cachedOrder = cacheManager.getCache("orders").get(order.getId(), Order.class);
        assertThat(cachedOrder).isNotNull();
        
        // Verify event publishing would be tested with Kafka listener
    }
}

Custom Test Container Configuration

@TestConfiguration
public class TestContainerConfig {
    
    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgreSQLContainer() {
        return new PostgreSQLContainer<>("postgres:13")
                .withDatabaseName("integration-tests-db")
                .withUsername("username")
                .withPassword("password")
                .withInitScript("init-test-data.sql");
    }
    
    @Bean
    @ServiceConnection
    public RedisContainer redisContainer() {
        return new RedisContainer("redis:6-alpine")
                .withExposedPorts(6379);
    }
}

// Shared container across test classes
abstract class BaseIntegrationTest {
    
    protected static final PostgreSQLContainer<?> POSTGRES;
    
    static {
        POSTGRES = new PostgreSQLContainer<>("postgres:13")
                .withDatabaseName("testdb")
                .withUsername("test")
                .withPassword("test");
        POSTGRES.start();
    }
    
    @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);
    }
}

// Test classes extend base
class OrderIntegrationTest extends BaseIntegrationTest {
    // Test implementation
}

class CustomerIntegrationTest extends BaseIntegrationTest {
    // Test implementation
}

Configuration and Profiles

Test Configuration

@TestConfiguration
public class IntegrationTestConfig {
    
    @Bean
    @Primary
    @Profile("integration-test")
    public Clock testClock() {
        // Fixed time for predictable tests
        return Clock.fixed(
            LocalDateTime.of(2024, 1, 1, 12, 0, 0)
                        .atZone(ZoneId.systemDefault())
                        .toInstant(),
            ZoneId.systemDefault()
        );
    }
    
    @Bean
    @Primary
    @Profile("integration-test")
    public EmailService mockEmailService() {
        return Mockito.mock(EmailService.class);
    }
    
    @Bean
    @ConditionalOnProperty(name = "test.external-services.enabled", havingValue = "false")
    public PaymentService mockPaymentService() {
        PaymentService mock = Mockito.mock(PaymentService.class);
        
        // Default stubbing for successful payment
        when(mock.processPayment(any())).thenReturn(
            new PaymentResult("txn_test_123", PaymentStatus.SUCCESS, BigDecimal.valueOf(100))
        );
        
        return mock;
    }
}

// Test-specific properties
@SpringBootTest
@ActiveProfiles("integration-test")
@TestPropertySource(properties = {
    "spring.jpa.hibernate.ddl-auto=create-drop",
    "spring.jpa.show-sql=true",
    "logging.level.org.springframework.web=DEBUG",
    "test.external-services.enabled=false"
})
class ConfiguredIntegrationTest {
    
    @Autowired
    private Clock clock;
    
    @Test
    void shouldUseFixedTimeForTests() {
        LocalDateTime now = LocalDateTime.now(clock);
        assertThat(now).isEqualTo(LocalDateTime.of(2024, 1, 1, 12, 0, 0));
    }
}

Environment-Specific Testing

// Development environment integration test
@SpringBootTest
@ActiveProfiles("dev")
class DevelopmentIntegrationTest {
    
    @Test
    void shouldConnectToDevelopmentDatabase() {
        // Test with development database
    }
}

// Production-like environment test
@SpringBootTest
@ActiveProfiles("prod-test")
@TestPropertySource(locations = "classpath:application-prod-test.properties")
class ProductionLikeIntegrationTest {
    
    @Test
    void shouldBehaveLikeProduction() {
        // Test with production-like configuration
    }
}

// Test configuration class with conditional beans
@Configuration
@Profile("test")
public class TestEnvironmentConfig {
    
    @Bean
    @ConditionalOnProperty(name = "test.use-real-services", havingValue = "false", matchIfMissing = true)
    public PaymentGateway mockPaymentGateway() {
        return Mockito.mock(PaymentGateway.class);
    }
    
    @Bean
    @ConditionalOnProperty(name = "test.use-real-services", havingValue = "true")
    public PaymentGateway realPaymentGateway() {
        return new SandboxPaymentGateway();
    }
}

Best Practices

Test Data Management

@Component
public class TestDataFactory {
    
    @Autowired
    private CustomerRepository customerRepository;
    
    @Autowired
    private ProductRepository productRepository;
    
    public Customer createCustomer(String email) {
        Customer customer = new Customer();
        customer.setFirstName("Test");
        customer.setLastName("Customer");
        customer.setEmail(email);
        return customerRepository.save(customer);
    }
    
    public Product createProduct(String name, BigDecimal price) {
        Product product = new Product();
        product.setName(name);
        product.setPrice(price);
        product.setStockQuantity(100);
        return productRepository.save(product);
    }
    
    public Order createOrder(Customer customer, Product... products) {
        Order order = new Order();
        order.setCustomer(customer);
        order.setStatus(OrderStatus.PENDING);
        
        BigDecimal total = BigDecimal.ZERO;
        for (Product product : products) {
            OrderItem item = new OrderItem();
            item.setProduct(product);
            item.setQuantity(1);
            item.setPrice(product.getPrice());
            order.addItem(item);
            total = total.add(product.getPrice());
        }
        
        order.setTotalAmount(total);
        return order;
    }
}

// Usage in tests
@SpringBootTest
class OrderIntegrationTest {
    
    @Autowired
    private TestDataFactory testDataFactory;
    
    @Autowired
    private OrderService orderService;
    
    @Test
    void shouldProcessOrderCorrectly() {
        // Arrange
        Customer customer = testDataFactory.createCustomer("[email protected]");
        Product product = testDataFactory.createProduct("Test Product", BigDecimal.valueOf(29.99));
        
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(customer.getId());
        request.addItem(new OrderItemRequest(product.getId(), 2, product.getPrice()));
        
        // Act
        Order order = orderService.createOrder(request);
        
        // Assert
        assertThat(order)
            .isNotNull()
            .satisfies(o -> {
                assertThat(o.getCustomer().getId()).isEqualTo(customer.getId());
                assertThat(o.getTotalAmount()).isEqualTo(BigDecimal.valueOf(59.98));
            });
    }
}

Test Organization and Cleanup

@SpringBootTest
@Transactional
class OrganizedIntegrationTest {
    
    @Autowired
    private TestDataFactory testDataFactory;
    
    private Customer testCustomer;
    private Product testProduct;
    
    @BeforeEach
    void setUp() {
        // Setup common test data
        testCustomer = testDataFactory.createCustomer("test" + System.nanoTime() + "@example.com");
        testProduct = testDataFactory.createProduct("Test Product", BigDecimal.valueOf(19.99));
    }
    
    @Nested
    @DisplayName("Order Creation Tests")
    class OrderCreationTests {
        
        @Test
        @DisplayName("Should create order with valid data")
        void shouldCreateOrderWithValidData() {
            // Test implementation using testCustomer and testProduct
        }
        
        @Test
        @DisplayName("Should reject order with insufficient inventory")
        void shouldRejectOrderWithInsufficientInventory() {
            // Test implementation
        }
    }
    
    @Nested
    @DisplayName("Order Processing Tests")
    class OrderProcessingTests {
        
        @Test
        @DisplayName("Should process payment and update order status")
        void shouldProcessPaymentAndUpdateOrderStatus() {
            // Test implementation
        }
        
        @Test
        @DisplayName("Should handle payment failure gracefully")
        void shouldHandlePaymentFailureGracefully() {
            // Test implementation
        }
    }
    
    @AfterEach
    void tearDown() {
        // Cleanup if needed (usually handled by @Transactional rollback)
    }
}

Performance and Monitoring

@SpringBootTest
class PerformanceIntegrationTest {
    
    @Autowired
    private OrderService orderService;
    
    @Test
    @Timeout(value = 5, unit = TimeUnit.SECONDS)
    void shouldCompleteOrderProcessingWithinTimeout() {
        // Test that critical operations complete within acceptable time
        CreateOrderRequest request = new CreateOrderRequest();
        request.setCustomerId(1L);
        
        assertDoesNotThrow(() -> {
            Order order = orderService.createOrder(request);
            assertThat(order).isNotNull();
        });
    }
    
    @Test
    void shouldHandleConcurrentOrderCreation() throws InterruptedException {
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);
        List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
        
        for (int i = 0; i < threadCount; i++) {
            final int customerId = i + 1;
            new Thread(() -> {
                try {
                    CreateOrderRequest request = new CreateOrderRequest();
                    request.setCustomerId((long) customerId);
                    orderService.createOrder(request);
                } catch (Exception e) {
                    exceptions.add(e);
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        latch.await(10, TimeUnit.SECONDS);
        
        assertThat(exceptions).isEmpty();
    }
    
    @Test
    void shouldMaintainDataConsistencyUnderLoad() {
        // Test data consistency with multiple concurrent operations
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List<Future<Order>> futures = new ArrayList<>();
        
        for (int i = 0; i < 20; i++) {
            final int orderId = i;
            futures.add(executor.submit(() -> {
                CreateOrderRequest request = new CreateOrderRequest();
                request.setCustomerId(1L);
                return orderService.createOrder(request);
            }));
        }
        
        List<Order> orders = futures.stream()
            .map(future -> {
                try {
                    return future.get();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.toList());
        
        assertThat(orders).hasSize(20);
        assertThat(orders.stream().map(Order::getId).distinct().count()).isEqualTo(20);
        
        executor.shutdown();
    }
}

Summary

Integration testing ensures that different components of your application work together correctly:

Key Benefits:

  • Validates component interactions and integrations
  • Tests real database operations and transactions
  • Verifies external service integrations
  • Ensures proper configuration and environment setup

Spring Boot Testing Features:

  • @SpringBootTest: Full application context testing
  • Test Slices: Focused testing (@WebMvcTest, @DataJpaTest, @JsonTest)
  • Test Containers: Real database testing with Docker
  • MockMvc: Web layer integration testing
  • TestRestTemplate: REST API testing

Best Practices:

  • Use appropriate test slices for focused testing
  • Implement proper test data management and cleanup
  • Test both happy path and error scenarios
  • Use Test Containers for realistic database testing
  • Mock external dependencies appropriately
  • Organize tests with clear naming and structure

Performance Considerations:

  • Use @Transactional for automatic rollback
  • Share expensive resources across tests when possible
  • Use profiles to configure test environments
  • Monitor test execution times and optimize slow tests

Integration testing with Spring Boot provides comprehensive coverage of your application's behavior in realistic environments, ensuring reliability and quality in production deployments.