簡介
整合測試測試多個元件與真實資料庫、真實配置一起工作。測試容器允許在 Docker 容器中自動執行 PostgreSQL、Redis、Kafka,確保類似生產的測試環境。
1.@SpringBootTest
1.1 完整的應用程式上下文
@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. 測試切片 — 僅載入必要的部分
2.1 @DataJpaTest — 測試儲存庫層
@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 — 測試控制器層
@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. 測試容器
3.1 設置
// build.gradle.kts
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:junit-jupiter")
3.2 PostgreSQL 容器
@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 共享容器模式(最佳化效能)
// 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 多個容器
@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. 測試資料管理
4.1 @Sql — 從檔案載入測試數據
@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 — 輔助方法
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");
}
}
總結
- 測試切片(@DataJpaTest、@WebMvcTest)僅載入必要的部分,運行速度比@SpringBootTest更快
- 測試容器自動啟動 PostgreSQL、Redis 容器 — 使用真實基礎架構進行測試
- @ServiceConnection 自動設定連線屬性,無需手動配置
- 共用容器模式在測試類別之間重複使用容器,減少啟動時間
練習
1.使用PostgreSQL Testcontainers為ProductRepository編寫@DataJpaTest:測試儲存、findAll、自訂查詢方法 2. 使用共用 PostgreSQL 容器建立 BaseIntegrationTest 類,為訂單 CRUD 流程編寫完整的整合測試 3.測試快取:使用Testcontainers Redis,驗證資料是否被快取並正確驅逐