本章介紹軟體測試的工程實踐,包括測試環境管理、測試資料管理、持續測試整合,以及測試報告與度量。

測試計劃#

測試計劃的重要性在敏捷開發模式下依然存在,只是從一次性集中制定變成了以迭代的方式持續制定。

測試計劃的五大要素#

要素核心問題內容
測試範圍測什麼?不測什麼?明確測試邊界
測試策略先測什麼?如何測?測試優先級和方法
測試資源誰來測?在哪測?人員和環境
測試進度何時開始?何時完成?時間表和里程碑
風險預估可能出什麼問題?風險識別和應對

測試策略制定#

測試策略考慮因素:
├── 測試類型選擇
│   ├── 功能測試 ─── 核心業務流程優先
│   ├── 效能測試 ─── 關鍵效能指標
│   ├── 安全測試 ─── 高風險功能
│   └── 兼容性測試 ─── 目標使用者設備
├── 自動化策略
│   ├── 哪些適合自動化
│   ├── 框架選型
│   └── 投入產出比
└── 回歸策略
    └── 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: 持續最佳化級
    └── 持續改進測試過程,追求卓越

測試不是終點,而是持續改進的過程。保持學習心態,關注測試技術的發展,不斷提升測試能力。