本章介紹軟體測試的工程實踐,包括測試環境管理、測試資料管理、持續測試整合,以及測試報告與度量。
測試計劃#
測試計劃的重要性在敏捷開發模式下依然存在,只是從一次性集中制定變成了以迭代的方式持續制定。
測試計劃的五大要素#
| 要素 | 核心問題 | 內容 |
|---|---|---|
| 測試範圍 | 測什麼?不測什麼? | 明確測試邊界 |
| 測試策略 | 先測什麼?如何測? | 測試優先級和方法 |
| 測試資源 | 誰來測?在哪測? | 人員和環境 |
| 測試進度 | 何時開始?何時完成? | 時間表和里程碑 |
| 風險預估 | 可能出什麼問題? | 風險識別和應對 |
測試策略制定#
測試策略考慮因素:
├── 測試類型選擇
│ ├── 功能測試 ─── 核心業務流程優先
│ ├── 效能測試 ─── 關鍵效能指標
│ ├── 安全測試 ─── 高風險功能
│ └── 兼容性測試 ─── 目標使用者設備
├── 自動化策略
│ ├── 哪些適合自動化
│ ├── 框架選型
│ └── 投入產出比
└── 回歸策略
└── 20% 精力覆蓋 80% 核心場景測試環境管理#
環境類型#
| 環境 | 用途 | 資料 |
|---|---|---|
| 開發環境 | 開發自測 | 開發資料 |
| 測試環境 | 功能測試 | 測試資料 |
| 預發布環境 | 上線前驗證 | 脫敏生產資料 |
| 生產環境 | 真實使用者使用 | 真實資料 |
環境管理挑戰#
常見問題及解決方案:
├── 環境不一致 ─────────→ 容器化、基礎設施即程式碼
├── 環境爭用 ─────────────→ 環境隔離、動態創建
├── 組態管理混亂 ─────────→ 組態中心、版本控制
└── 資料同步困難 ─────────→ 資料管理平台容器化測試環境#
# Docker Compose 測試環境範例
version: "3"
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- db
- redis
environment:
- SPRING_PROFILES_ACTIVE=test
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=test
- MYSQL_DATABASE=testdb
redis:
image: redis:6基礎設施即程式碼:使用 Docker、Kubernetes 等工具將測試環境組態程式碼化,確保環境的一致性和可重複性。
測試資料管理#
資料準備策略#
| 策略 | 說明 | 適用場景 |
|---|---|---|
| On-the-fly | 測試時即時創建 | 一次性資料(訂單、優惠券) |
| Out-of-box | 預先準備好 | 穩定資料(商品類目、品牌) |
| 混合方式 | 結合兩種方式 | 實際項目中最常用 |
資料創建技術#
資料創建方式:
├── API 呼叫
│ ├── 優點:資料準確性高
│ └── 缺點:不是所有資料都有 API
├── 資料庫操作
│ ├── 優點:效率高、靈活
│ └── 缺點:需要維護 SQL 與程式碼同步
└── 綜合方式
└── API 創建基礎資料,SQL 修改特定狀態測試資料工具設計#
// 測試資料工具範例
public class TestDataFactory {
// 創建測試使用者
public User createUser() {
return createUser(UserType.NORMAL);
}
public User createUser(UserType type) {
User user = apiClient.createUser(generateUserData());
if (type == UserType.VIP) {
// 通過 SQL 修改使用者類型
jdbcTemplate.update(
"UPDATE users SET type = 'VIP' WHERE id = ?",
user.getId()
);
}
return user;
}
// 創建測試訂單
public Order createOrder(User buyer, Product product) {
return apiClient.createOrder(
buyer.getId(),
product.getId()
);
}
}資料隔離原則#
測試資料污染是自動化測試不穩定的主要原因之一。每個測試案例應該創建自己的資料,或使用唯一標識區分。
資料隔離策略:
├── 唯一標識 ─── 測試資料帶唯一前綴/後綴
├── 事務回滾 ─── 測試結束後回滾資料變更
├── 獨立資料集 ─── 每個測試案例使用獨立資料
└── 清理策略 ─── 測試後自動清理測試資料持續測試(CI/CD 整合)#
測試金字塔在 CI/CD 中的應用#
┌─────────────────────────────────────────────────────────┐
│ CI/CD 測試流水線 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 程式碼提交 │ │
│ └────────────────────┬────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 單元測試 (秒級) │ │
│ │ - 每次提交都執行 │ │
│ │ - 阻斷部署條件:失敗 │ │
│ └────────────────────┬────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 整合測試 (分鐘級) │ │
│ │ - 每次提交或合併請求執行 │ │
│ │ - 阻斷部署條件:失敗 │ │
│ └────────────────────┬────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ E2E 測試 (10-30 分鐘) │ │
│ │ - 部署到測試環境後執行 │ │
│ │ - 阻斷部署條件:關鍵案例失敗 │ │
│ └────────────────────┬────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 效能/安全測試 (定時或手動觸發) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Jenkins Pipeline 範例#
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
jacoco(execPattern: '**/target/jacoco.exec')
}
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -P integration-test'
}
}
stage('Deploy to Test') {
steps {
sh 'deploy-to-test.sh'
}
}
stage('E2E Tests') {
steps {
sh 'mvn verify -P e2e-test'
}
post {
always {
publishHTML([
reportDir: 'target/e2e-reports',
reportFiles: 'index.html',
reportName: 'E2E Test Report'
])
}
}
}
}
post {
failure {
slackSend channel: '#dev-alerts',
message: "Build Failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
}
}
}測試失敗處理策略#
| 策略 | 說明 | 適用場景 |
|---|---|---|
| 快速失敗 | 發現失敗立即停止 | 單元測試 |
| 繼續執行 | 記錄失敗,繼續其他測試 | 整合測試、E2E 測試 |
| 重試機制 | 失敗後自動重試 | 不穩定的 E2E 測試 |
| 智慧跳過 | 根據程式碼變更選擇性執行 | 大型測試套件 |
遺留程式碼的漸進式測試導入#
遺留程式碼最棘手的弔詭是:它沒有測試所以難以安全修改,而它的設計又難以加上測試。 對這類程式碼,目標不是一次補齊覆蓋率,而是「在每次必須觸碰它時,順手把碰到的那一小塊變得可測試」。
flowchart LR
A[遺留程式碼<br/>無測試] --> B[特性化測試<br/>鎖住現況]
B --> C[找接縫 Seam<br/>插入測試點]
C --> D[抽芽方法<br/>新邏輯隔離可測]
D --> E[逐步擴大<br/>安全綠色區域]第一步:特性化測試(Characterization Test)鎖住現況
面對一段你也看不懂、但「目前是對的」的程式碼,先別管它「應該」回傳什麼,而是寫測試把它「現在實際」回傳什麼釘死下來。這層保護網讓你後續重構時,任何行為改變都會立刻被偵測到。
// 不問「應該是多少」,先問「現在是多少」,把現況釘住
@Test
void characterize_legacyFeeCalc() {
// 執行一次,看實際輸出,再把它寫成斷言
assertEquals(127.5, legacy.calcFee(1000, "VIP", 3));
}特性化測試記錄的是「現況」而非「正確規格」——其中可能包含既有 bug。它的價值在於:先凍結行為,讓你能安全地動程式碼;釐清正確規格後,再逐條把它升級成真正的規格測試。
第二步:找接縫(Seam)插入測試點
接縫是「不修改該處原始碼,就能改變其行為」的位置。最常見的是把寫死的 new、靜態呼叫、全域狀態,改成可由外部注入的相依——這樣測試就能在該處塞入替身。
| 接縫類型 | 原本寫死的形式 | 改成可注入的測試點 |
|---|---|---|
| 物件接縫 | 方法內 new PaymentApi() | 由建構子 / 參數注入相依 |
| 多型接縫 | 直接呼叫具體類別 | 透過介面呼叫,測試傳入替身 |
| 參數接縫 | 內部讀取系統時間 / 環境 | 把時間、組態當參數傳入 |
第三步:抽芽方法(Sprout Method)
當需要在一段難以測試的大方法裡加新邏輯時,不要把新程式碼直接塞進去。把新邏輯抽成一個獨立、乾淨、可單獨測試的新方法(「芽」),舊方法只負責呼叫它。新功能因此一誕生就被測試覆蓋,舊程式碼維持原狀、風險最小。
// 舊方法龐大且無測試,只在其中插入一行呼叫新「芽」
void processOrder(Order o) {
// ...大量既有、未測試的邏輯...
o.setShippingFee(calcShippingFee(o)); // 新邏輯抽成芽方法
// ...大量既有邏輯...
}
// 芽方法獨立、純粹、可直接單元測試
double calcShippingFee(Order o) {
return o.isVip() ? 0.0 : 60.0;
}接縫辨識、抽芽方法在 Java 中的具體操作(依賴注入重構、以介面取代具體類別、隔離框架運用)見 Java 單元測試深度章節 的重構與依賴控制單元。
測試報告與度量#
關鍵測試指標#
測試度量指標:
├── 測試覆蓋率
│ ├── 需求覆蓋率
│ └── 程式碼覆蓋率(行/分支/條件)
├── 測試效率
│ ├── 案例執行時間
│ ├── 缺陷發現率
│ └── 缺陷逃逸率
├── 測試品質
│ ├── 案例通過率
│ ├── 缺陷重開率
│ └── 自動化穩定性
└── 測試進度
├── 案例完成率
└── 缺陷趨勢缺陷報告要素#
| 要素 | 說明 |
|---|---|
| 標題 | 在什麼情況下發生了什麼問題 |
| 概述 | 缺陷本質的概括性描述 |
| 影響 | 對使用者或業務的影響 |
| 環境 | 復現所需的環境資訊 |
| 前置條件 | 開始復現前的系統狀態 |
| 復現步驟 | 可操作的、連貫的步驟 |
| 期望/實際結果 | 應該發生什麼 vs 實際發生什麼 |
| 優先級/嚴重程度 | 修復的緊急程度和影響程度 |
好的缺陷報告不是大量資訊的堆疊,而是以高效的方式提供準確有用的資訊。
測試報告自動化#
自動化測試報告生成:
├── JUnit/TestNG XML 報告
├── JaCoCo 覆蓋率報告
├── Allure 富報告
│ ├── 測試案例詳情
│ ├── 歷史趨勢
│ ├── 附件(截圖、日誌)
│ └── 缺陷關聯
└── 自定義 Dashboard
└── Grafana + InfluxDB/Prometheus測試團隊協作#
測試工程師的角色#
測試工程師是項目的「潤滑劑」:
├── 對接產品經理 ─── 確保需求正確實現
├── 對接開發人員 ─── 確保缺陷及時修復
├── 對接運維人員 ─── 確保環境穩定
└── 對接使用者 ─────── 確保使用者體驗敏捷測試實踐#
| 實踐 | 說明 |
|---|---|
| 測試左移 | 測試人員早期介入需求分析 |
| 持續測試 | 自動化測試融入 CI/CD |
| 探索性測試 | 邊測試邊學習,發現未知問題 |
| 結對測試 | 開發和測試結對工作 |
| BDD | 行為驅動開發,用自然語言描述測試 |
BDD 範例(Cucumber)#
# login.feature
Feature: 使用者登入
作為一個註冊使用者
我希望能夠登入系統
以便使用系統功能
Scenario: 正確的使用者名稱和密碼登入成功
Given 我已經是一個註冊使用者
And 我的使用者名稱是 "testuser"
And 我的密碼是 "password123"
When 我在登入頁面輸入使用者名稱和密碼
And 我點擊登入按鈕
Then 我應該看到首頁
And 我應該看到歡迎資訊 "Hello, testuser"
Scenario: 錯誤的密碼登入失敗
Given 我已經是一個註冊使用者
And 我的使用者名稱是 "testuser"
When 我輸入錯誤的密碼 "wrongpassword"
And 我點擊登入按鈕
Then 我應該看到錯誤資訊 "使用者名稱或密碼錯誤"測試能力成熟度#
測試能力成熟度等級:
├── Level 1: 初始級
│ └── 測試是無序的、隨機的
├── Level 2: 可重複級
│ └── 有基本的測試流程,可重複執行
├── Level 3: 已定義級
│ └── 測試流程標準化,有專門的測試團隊
├── Level 4: 量化管理級
│ └── 測試過程可度量,有明確的品質指標
└── Level 5: 持續最佳化級
└── 持續改進測試過程,追求卓越測試不是終點,而是持續改進的過程。保持學習心態,關注測試技術的發展,不斷提升測試能力。