某創業公司 2002 年起全面採用 JUnit + Ant + CruiseControl 持續整合。初期測試秒級完成,隨著代碼庫增長,測試數達 2000 支,完整建構耗時超過 3 小時。團隊被迫將測試移至夜間跑批,導致回饋延遲、錯誤堆積、開發者對測試失去信心。本文揭示測試分類分層策略,將單次建構時間從數小時壓縮至分鐘級,恢復敏捷回饋迴路。
「我們寫單元測試」這句話極其模糊,如同「我家有動物」不知是貓還是熊。依執行時間、依賴範圍、撰寫成本將測試劃分為三個截然不同的層次:
| 層次 | 測試類型 | 執行時間 | 依賴範圍 | 撰寫難度 | 執行頻率 | 典型比例 |
|---|---|---|---|---|---|---|
| 底層 (最多) | 單元測試 | < 100ms/支 | 純記憶體,無外部依賴 (DB、FS、Network) | 低 | 每次編譯/存檔 (IDE 即時) | 70-80% |
| 中層 | 組件測試 | 100ms - 5s/支 | 含資料庫、檔案系統、外部服務 Mock/Embedded | 中 | 每次 Commit / CI Pipeline | 15-20% |
| 頂層 (最少) | 系統測試 | > 5s/支 (常達分鐘) | 完整部署環境,真實外部系統 | 高 | 每日/每發布 / 階段性 | 5-10% |
定義:驗證單一類別/函數的邏輯,完全隔離外部依賴 (資料庫、檔案、網路、時間、隨機數)。
new File(), DriverManager.getConnection(), HttpClient// 純單元測試範例:計算折扣邏輯
@Test
public void testVolumeDiscount() {
PricingEngine engine = new PricingEngine();
// 無任何外部依賴,純記憶體運算
assertEquals(90.0, engine.calculatePrice(100, 10), 0.001); // 10% off
assertEquals(80.0, engine.calculatePrice(100, 25), 0.001); // 20% off
}
// 使用 Mock 隔離 Repository
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository repo;
@InjectMocks OrderService service;
@Test
void testPlaceOrder() {
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
Order order = service.placeOrder(new OrderRequest("user1", List.of(item)));
assertNotNull(order.getId());
verify(repo).save(order);
}
}
定義:驗證多個類別協作或單一組件與基礎設施整合 (資料庫、檔案系統、訊息佇列)。使用嵌入式/輕量級實現替代生產環境。
@TempDir)// Spring Boot 整合測試 = 組件測試典型形式
@DataJpaTest // 自動配置 H2 + 測試事務回滾
@AutoConfigureTestDatabase(replace = Replace.NONE) // 用真實 Testcontainers DB
class OrderRepositoryTest {
@Autowired OrderRepository repo;
@Test
void testFindByUserId() {
User user = userRepository.save(new User("john"));
Order order = repo.save(new Order(user, BigDecimal.TEN));
List found = repo.findByUserId(user.getId());
assertEquals(1, found.size());
assertEquals(BigDecimal.TEN, found.get(0).getAmount());
}
}
// 檔案處理組件測試
@TempDir Path tempDir;
@Test
void testCsvExport() throws IOException {
File exportFile = tempDir.resolve("orders.csv").toFile();
new CsvExporter(exportFile).export(orders);
assertTrue(Files.size(exportFile.toPath()) > 0);
}
定義:在完整部署拓撲中驗證跨系統業務流程 (UI、API、批次、排程)。
// API 系統測試範例
@Tag("system")
class OrderApiSystemTest {
static final String BASE_URL = "https://test-env.company.com/api";
@Test
void testFullOrderFlow() {
// 1. 建立訂單
String orderId = given()
.baseUri(BASE_URL)
.body(new OrderRequest("user1", List.of(item)))
.post("/orders")
.then().statusCode(201)
.extract().jsonPath().getString("id");
// 2. 支付 (呼叫外部支付網關沙箱)
given().baseUri(BASE_URL)
.body(new PaymentRequest(orderId, "credit_card"))
.post("/payments")
.then().statusCode(200);
// 3. 驗證狀態變更
given().baseUri(BASE_URL)
.get("/orders/" + orderId)
.then().statusCode(200)
.body("status", equalTo("PAID"));
}
}
maven-surefire-plugin
**/*UnitTest.java
**/*Test.java
**/*ComponentTest.java
**/*SystemTest.java
maven-failsafe-plugin
integration-test verify
**/*ComponentTest.java
**/*SystemTest.java
stages:
- name: fast-feedback # < 5 分鐘
script: mvn test # 僅單元測試
trigger: on every push
- name: component-verification # < 15 分鐘
script: mvn verify -DskipUnitTests=true
trigger: on merge to main / PR
- name: system-validation # 30-60 分鐘
script: mvn verify -Dtest=*SystemTest
trigger: nightly / on release tag
- name: performance-smoke # 額外階段
script: k6 run smoke.js
trigger: on release tag
| 手段 | 單元測試 | 組件測試 | 系統測試 |
|---|---|---|---|
| 命名慣例 | *UnitTest, *Test |
*ComponentTest, *IntegrationTest |
*SystemTest, *E2ETest |
| JUnit 標籤 (JUnit 5+) | @Tag("unit") |
@Tag("component") |
@Tag("system") |
| 目錄結構 | src/test/java/unit/ |
src/test/java/component/ |
src/test/java/system/ |
| 建構工具配置 | Surefire (預設) | Failsafe / 獨立 profile | Failsafe / 獨立階段 |
| 資料庫策略 | 無 / Mock | H2 / Testcontainers | 真實測試庫 |
某專案實施分層前後對比:
| 指標 | 分層前 | 分層後 | 改善 |
|---|---|---|---|
| 完整建構時間 | 180 分鐘 | 3 分鐘 (單元) + 12 分鐘 (組件) + 45 分鐘 (系統) | 快速回饋 60x |
| 開發者單次執行 | 不執行 (太慢) | 每次存檔自動跑單元測試 | TDD 可行 |
| CI 回饋延遲 | 隔天早晨 | 推送後 3 分鐘 | 即時修復 |
| 測試維護成本 | 高 (混雜難改) | 低 (職責清晰) | 可持續 |
@Autowired、DataSource、Testcontainers 即為分類失敗-T 4, Gradle --parallel),系統測試謹慎並行測試分類不是學術細分,而是工程效能的生存技能。將測試劃分為單元 (毫秒、隔離、高頻)、組件 (秒級、真實基礎設施、中頻)、系統 (分鐘級、完整環境、低頻) 三層,配合建構工具與 CI/CD 管線分階段執行,能將「三小時建構」壓縮為「三分鐘快速回饋」,讓測試真正成為敏捷開發的加速器而非煞車塊。