自動化測試是將人對軟體的測試行為轉化為由機器執行的實踐,能夠大幅提升測試效率和一致性。
什麼是自動化測試?#
自動化測試的本質:先寫一段程式碼,然後去測試另一段程式碼。實現自動化測試本身屬於開發工作,需要投入時間和精力。
自動化測試的價值#
| 優勢 | 說明 |
|---|
| 替代重複性工作 | 讓測試工程師專注於用例設計和新功能測試 |
| 提升回歸效率 | 非常適合敏捷開發的頻繁回歸需求 |
| 利用無人值守時間 | 夜間執行測試,白天分析結果 |
| 實現特殊測試 | 7×24 穩定性測試、高並行壓力測試 |
| 保證一致性 | 避免人為遺漏或疏忽 |
自動化測試的局限#
業界玩笑話:「開發手一抖,自動化測試忙一宿」——自動化測試的維護成本是實施前必須考慮的關鍵因素。
| 局限 | 說明 |
|---|
| 不能取代手工測試 | 只能替代高頻率、機械化的步驟 |
| 維護成本高 | 被測系統變化時需要更新用例 |
| 開發工作量大 | 執行次數 ≥ 5 次才能收回成本 |
| 依賴用例品質 | 不穩定的用例比沒有更糟糕 |
適合自動化測試的場景#
適合自動化的條件檢查清單:
├── ☑ 需求穩定,不頻繁變更
├── ☑ 研發周期長,需要頻繁回歸
├── ☑ 需要跨平台重複執行
├── ☑ 手工測試無法實現(如高並行)
├── ☑ 開發過程規範,具備可測試性
└── ☑ 團隊具備編程能力
ROI 分析#
自動化測試 ROI = (手工測試成本 × 執行次數 - 自動化開發成本 - 維護成本) / 自動化投入
經驗法則:
- 執行次數 ≥ 5 次,自動化才划算
- 20% 的精力覆蓋 80% 的回歸測試
單元測試#
單元測試是對軟體中最小可測試單元(通常是函式或類)進行隔離測試。
單元測試的價值#
電視機生產的類比:
├── 電子元器件 ─────────── 單元(函式/類)
├── 功能電路板 ─────────── 模組
└── 完整電視機 ─────────── 系統
先測試元器件,再組裝,比組裝後逐級排查問題效率高得多。
單元測試用例的組成#
單元測試用例是「輸入資料」和「預期輸出」的集合。但這兩者都比想像中複雜得多。
輸入資料包括:
- 被測函式的輸入參數
- 函式內部讀取的全局/靜態變數
- 函式內部讀取的成員變數
- 函式內部呼叫子函式獲得/改寫的資料
預期輸出包括:
- 被測函式的回傳值
- 被測函式的輸出參數
- 被測函式改寫的成員變數/全局變數
- 被測函式進行的文件/資料庫/訊息佇列更新
驅動程式碼、樁程式碼和 Mock 程式碼#
┌─────────────────────────────────────────────────┐
│ 測試程式碼 │
│ ┌──────────────────────────────────────────┐ │
│ │ 驅動程式碼 (Driver) │ │
│ │ - 呼叫被測函式 │ │
│ │ - 準備測試資料 │ │
│ │ - 驗證結果 │ │
│ └──────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ 被測函式 │ │
│ └──────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────┐ │
│ │ 樁程式碼 (Stub) / Mock 程式碼 │ │
│ │ - 代替被測函式呼叫的真實程式碼 │ │
│ │ - 控制執行路徑 │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Stub vs Mock#
| 特性 | Stub(樁程式碼) | Mock(模擬程式碼) |
|---|
| 目的 | 控制被測函式的執行路徑 | 驗證交互行為 |
| 關注點 | 回傳值 | 呼叫方式、參數、次數、順序 |
| Assert 位置 | 驅動程式碼中 | Mock 函式中 |
// Stub 示例:控制回傳值
@Test
void testWithStub() {
// Stub:讓 userService.getUser() 回傳固定值
when(userService.getUser(anyLong())).thenReturn(testUser);
// 執行被測方法
String result = orderService.createOrder(userId);
// 在驅動程式碼中驗證結果
assertEquals("SUCCESS", result);
}
// Mock 示例:驗證交互
@Test
void testWithMock() {
orderService.createOrder(userId);
// 驗證 Mock 是否被正確呼叫
verify(emailService, times(1)).sendConfirmation(any());
verify(inventoryService).reduceStock(productId, 1);
}
整合測試#
整合測試關注軟體模組之間的介面呼叫和資料傳遞。
整合測試 vs 單元測試#
| 方面 | 單元測試 | 整合測試 |
|---|
| 範圍 | 單個函式/類 | 多個模組協作 |
| 依賴處理 | 使用 Stub/Mock | 使用真實依賴 |
| 目標 | 驗證邏輯正確性 | 驗證介面協作 |
抽樁(Un-stub)#
單元測試階段:函式 A 呼叫 Stub 函式 B
↓
整合測試階段:將 Stub 函式 B 替換為真實函式 B(抽樁)
API 自動化測試#
API 測試的三大步驟:
// 1. 準備測試資料
UserDTO testUser = new UserDTO("test@example.com", "password123");
// 2. 發起 API 呼叫
Response response = given()
.contentType(ContentType.JSON)
.body(testUser)
.when()
.post("/api/users");
// 3. 驗證回傳結果
response.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("test@example.com"));
API 測試框架選擇#
| 框架 | 語言 | 特點 |
|---|
| REST Assured | Java | 流式 API,易於使用 |
| Postman/Newman | JS | 界面友好,支持 CI/CD |
| pytest + requests | Python | 靈活,社區活躍 |
| Karate | Java | DSL 風格,無需編碼 |
GUI 自動化測試#
Selenium 工作原理#
┌──────────────────────────────────────────────────────┐
│ 測試程式碼 (Client) │
│ driver.findElement(By.id("login")) │
└──────────────────────────────────────────────────────┘
│
HTTP Request
(WebDriver 協定)
↓
┌──────────────────────────────────────────────────────┐
│ Remote Server (WebDriver) │
│ 解析請求,呼叫瀏覽器原生 WebDriver │
└──────────────────────────────────────────────────────┘
│
原生 API 呼叫
↓
┌──────────────────────────────────────────────────────┐
│ 瀏覽器 │
│ 執行頁面操作,回傳結果 │
└──────────────────────────────────────────────────────┘
頁面物件模型(Page Object Model)#
頁面物件模型將頁面元素和操作封裝成獨立的類,使測試程式碼更易維護。
// 頁面物件類
public class LoginPage {
private WebDriver driver;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "login-btn")
private WebElement loginButton;
public void login(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
}
}
// 測試類
public class LoginTest {
@Test
void testLogin() {
LoginPage loginPage = new LoginPage(driver);
loginPage.login("user@example.com", "password");
// 驗證登錄成功
}
}
測試資料管理#
創建方式#
| 方式 | 說明 | 適用場景 |
|---|
| API 呼叫 | 通過產品 API 創建資料 | 資料準確性要求高 |
| 資料庫操作 | 直接操作資料庫 | API 不支持,需要大量資料 |
| 綜合方式 | API + 資料庫 | 創建特定狀態的資料 |
創建時機#
| 時機 | 說明 | 優缺點 |
|---|
| On-the-fly | 測試執行時實時創建 | 資料隔離好,但效率低 |
| Out-of-box | 預先創建好「開箱即用」 | 效率高,但可能被污染 |
最佳實踐:穩定資料(如商品類目)用 Out-of-box,一次性資料(如訂單)用 On-the-fly。
單元測試最佳實踐#
FIRST 原則#
| 原則 | 說明 |
|---|
| Fast | 快速執行,秒級反饋 |
| Independent | 用例之間相互獨立 |
| Repeatable | 任何環境下結果一致 |
| Self-validating | 自動判斷通過/失敗 |
| Timely | 與生產程式碼同時編寫 |
測試程式碼組織(AAA 模式)#
@Test
void shouldCalculateTotalPrice() {
// Arrange(準備)
ShoppingCart cart = new ShoppingCart();
cart.add(new Product("Apple", 10.0), 2);
cart.add(new Product("Banana", 5.0), 3);
// Act(執行)
double total = cart.calculateTotal();
// Assert(驗證)
assertEquals(35.0, total, 0.001);
}
實施單元測試的挑戰#
常見挑戰及解決方案
| 挑戰 | 解決方案 |
|---|
| 緊密耦合的程式碼難以隔離 | 重構程式碼,引入依賴注入 |
| 程式碼可測試性差 | 設計時考慮可測試性 |
| 無法模擬系統底層函式 | 使用 PowerMock 等工具 |
| 覆蓋率越往後越難提高 | 設定合理目標,不追求 100% |
自動化測試框架選型#
單元測試框架#
| 語言 | 框架 | 特點 |
|---|
| Java | JUnit 5, TestNG | 註解驅動,生態豐富 |
| Python | pytest, unittest | pytest 更靈活 |
| JavaScript | Jest, Mocha | Jest 內建 Mock |
| Go | testing + testify | 內建支持 |
Mock 框架#
| 語言 | 框架 |
|---|
| Java | Mockito, PowerMock |
| Python | unittest.mock, pytest-mock |
| JavaScript | Jest (內建), Sinon.js |
程式碼覆蓋率工具#
| 語言 | 工具 |
|---|
| Java | JaCoCo |
| JavaScript | Istanbul, nyc |
| Python | coverage.py |
| Go | go test -cover |