Introduction
Integration tests test multiple components working together with real database, real configuration. Testcontainers allow running PostgreSQL, Redis, Kafka in Docker containers automatically, ensuring a production-like test environment.
1. @SpringBootTest
1.1 Full Application Context
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldCreateAndRetrieveOrder() {
// Create
var request = new CreateOrderRequest(/* ... */);
ResponseEntity<OrderResponse> createResponse = restTemplate
.postForEntity("/api/v1/orders", request, OrderResponse.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Long orderId = createResponse.getBody().id();
// Retrieve
ResponseEntity<OrderResponse> getResponse = restTemplate
.getForEntity("/api/v1/orders/" + orderId, OrderResponse.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().id()).isEqualTo(orderId);
}
}
2. Test Slices — Load only the necessary part
2.1 @DataJpaTest — Test Repository Layer
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindProductsByCategory() {
// Given
Product p1 = Product.builder()
.name("Spring Boot Book").price(new BigDecimal("450000"))
.category("Education").build();
Product p2 = Product.builder()
.name("Java Mug").price(new BigDecimal("150000"))
.category("Merchandise").build();
entityManager.persist(p1);
entityManager.persist(p2);
entityManager.flush();
// When
List<Product> result = productRepository.findByCategory("Education");
// Then
assertThat(result).hasSize(1);
assertThat(result.getFirst().getName()).isEqualTo("Spring Boot Book");
}
@Test
void shouldSearchByKeyword() {
entityManager.persist(Product.builder()
.name("Spring Boot Guide").price(BigDecimal.TEN)
.category("Book").build());
entityManager.flush();
Page<Product> results = productRepository
.searchByKeyword("spring", Pageable.ofSize(10));
assertThat(results.getContent()).hasSize(1);
}
}
2.2 @WebMvcTest — Test Controller Layer
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private ProductService productService;
@Test
void shouldReturnProduct() throws Exception {
var product = new ProductResponse(1L, "Book", new BigDecimal("450000"));
when(productService.getProduct(1L)).thenReturn(product);
mockMvc.perform(get("/api/v1/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Book"))
.andExpect(jsonPath("$.price").value(450000));
}
}
3. Testcontainers
3.1 Setup
// build.gradle.kts
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:junit-jupiter")
3.2 PostgreSQL Containers
@SpringBootTest
@Testcontainers
class ProductRepositoryIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:17-alpine");
@Autowired
private ProductRepository productRepository;
@Test
void shouldSaveAndRetrieveProduct() {
Product product = Product.builder()
.name("Test Product")
.price(new BigDecimal("100000"))
.category("Test")
.build();
Product saved = productRepository.save(product);
assertThat(saved.getId()).isNotNull();
assertThat(productRepository.findById(saved.getId()))
.isPresent()
.hasValueSatisfying(p ->
assertThat(p.getName()).isEqualTo("Test Product"));
}
}
3.3 Shared Container Pattern (optimized performance)
// Base class cho tất cả integration tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public abstract class BaseIntegrationTest {
@Container
@ServiceConnection
static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:17-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
protected TestRestTemplate restTemplate;
}
// Test kế thừa, reuse container
class OrderIntegrationTest extends BaseIntegrationTest {
@Autowired
private OrderRepository orderRepository;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
}
@Test
void shouldCreateOrder() {
// Test with real PostgreSQL
}
}
3.4 Multiple containers
@SpringBootTest
@Testcontainers
class FullStackIntegrationTest {
@Container
@ServiceConnection
static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:17-alpine");
@Container
@ServiceConnection
static final GenericContainer<?> redis =
new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
}
4. Test Data Management
4.1 @Sql — Load test data from file
@DataJpaTest
@Sql("/test-data/products.sql")
class ProductRepositoryTest {
@Test
@Sql(statements = "DELETE FROM products", executionPhase = AFTER_TEST_METHOD)
void shouldFindAllProducts() {
List<Product> products = productRepository.findAll();
assertThat(products).hasSize(5); // Từ products.sql
}
}
4.2 TestFixtures — Helper methods
public class TestFixtures {
public static Product aProduct() {
return Product.builder()
.name("Test Product")
.price(new BigDecimal("100000"))
.category("Test")
.build();
}
public static CreateProductRequest aCreateProductRequest() {
return new CreateProductRequest("Test Product",
new BigDecimal("100000"), "Test");
}
}
Summary
- Test Slices (@DataJpaTest, @WebMvcTest) loads only the necessary parts, runs faster than @SpringBootTest
- Testcontainers automatically spin up PostgreSQL, Redis containers — test with real infrastructure
- @ServiceConnection automatically configures connection properties, no manual config required
- Shared Container Pattern reuses containers between test classes, reducing startup time
Exercises
- Write @DataJpaTest for ProductRepository with PostgreSQL Testcontainers: test save, findAll, custom query methods
- Create BaseIntegrationTest class with shared PostgreSQL container, write full integration test for Order CRUD flow
- Test caching: use Testcontainers Redis, verify data is cached and evict correctly