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
- Spring Boot Integration Testing
- Database Integration Testing
- Web Layer Testing
- External Service Testing
- Test Containers
- Configuration and Profiles
- Best Practices
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.