Chuyển đến nội dung chính

第 18 課:整合測試 — @SpringBootTest 和 Testcontainers

@SpringBootTest、@DataJpaTest 用於整合測試。測試容器 — 在 Docker 中使用真實的 PostgreSQL、Redis 進行測試。測試切片和最佳實踐。

💻 程式設計 — 第 17 課 第 18 課:整合測驗 — @SpringBootTest 和測試容器

Spring Boot 4:從基礎到高級

第 5 部分:測試和程式碼質量

亞洲開發網

簡介

整合測試測試多個元件與真實資料庫、真實配置一起工作。測試容器允許在 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,驗證資料是否被快取並正確驅逐