什麼是 Unit Testing (單元測試)?

1
什麼是 Unit Testing (單元測試)?:單元測試(Unit Testing)是軟體開發中針對程式碼最小可測單位(如單一函式、方法或類別)進行隔離驗證的自動化測試實踐,確保每個單元按預期正確運作。它透過輸入特定資料、執行程式碼並比對實際輸出與期望結果,及早發現錯誤、提升程式碼品質,是 TDD(測試導向開發)與現代 DevOps 的基礎實踐。

什麼是 Unit Testing (單元測試)?

單元測試(Unit Testing)是軟體開發中針對程式碼最小可測單位(如單一函式、方法或類別)進行隔離驗證的自動化測試實踐,確保每個單元按預期正確運作。它透過輸入特定資料、執行程式碼並比對實際輸出與期望結果,及早發現錯誤、提升程式碼品質,是 TDD(測試導向開發)與現代 DevOps 的基礎實踐。

 

單元測試的核心原則(FIRST)

優質單元測試必須符合 FIRST 五原則:

  • Fast(快速):單測試 < 100ms,CI 管道幾秒完成

  • Independent(獨立):測試互不影響,隨機順序皆通過

  • Repeatable(可重現):每次執行結果一致

  • Self-Validating(自我驗證):自動判定通過/失敗,無人工判讀

  • Timely(及時):開發同一功能時立即撰寫

 

單元測試 vs 其他測試類型

測試類型 目標 隔離性 執行速度 範例
單元測試 函式/方法 完全隔離(Mock 外部) 最快 calculateDiscount(1000, 0.1)
整合測試 模組協作 部分依賴真實元件 中等 API + DB 連線
端到端測試 完整流程 全真實環境 登入→購物車→結帳
效能測試 負載能力 真實環境 1 萬併發 RPS

 

程式語言測試框架範例

Python(pytest)

# calc.py
def add(a: int, b: int) -> int:
    return a + b

def divide(a: int, b: int) -> float:
    if b == 0:
        raise ValueError("除數不能為零")
    return a / b

# test_calc.py
import pytest
from calc import add, divide

def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

def test_divide_zero():
    with pytest.raises(ValueError):
        divide(10, 0)

def test_divide_normal():
    assert divide(10, 2) == 5.0

# 執行:pytest test_calc.py -v
 

JavaScript(Jest)

// math.js
export function sum(a, b) {
    return a + b;
}

export function divide(a, b) {
    if (b === 0) throw new Error('Division by zero');
    return a / b;
}

// math.test.js
import { sum, divide } from './math';

test('should add positive numbers', () => {
    expect(sum(2, 3)).toBe(5);
});

test('divide by zero throws error', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
});

test('divide works correctly', () => {
    expect(divide(10, 2)).toBe(5);
});

// 執行:npx jest
 

Java(JUnit 5)

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    @Test
    void addPositiveNumbers() {
        assertEquals(5, calculator.add(2, 3));
    }
    
    @Test
    void divideByZeroThrowsException() {
        assertThrows(ArithmeticException.class, 
            () -> calculator.divide(10, 0));
    }
}

 

AAA 測試結構模式

標準單元測試遵循 Arrange-Act-Assert(安排-執行-斷言)模式:

def test_calculate_discount():  # AAA 結構
    # Arrange(安排):準備測試資料
    price = 1000
    discount_rate = 0.2
    expected = 800
    
    # Act(執行):呼叫待測函式
    result = calculate_discount(price, discount_rate)
    
    # Assert(斷言):驗證結果
    assert result == expected
 

Mocking 與隔離測試

單元測試必須隔離外部依賴,使用 Mock 模擬:

# 依賴真實 API/DB(不穩定)
def test_process_order():
    response = requests.get("https://api.example.com")  # 網路不穩
    process_order(response)

# Mock 外部依賴
@patch('requests.get')
def test_process_order(mock_get, mocker):
    mock_get.return_value.json.return_value = {'status': 'success'}
    result = process_order()
    mock_get.assert_called_once()
    assert result == 'processed'
 

測試覆蓋率(Code Coverage)

衡量測試品質的關鍵指標:

陳述式覆蓋率:執行過的程式碼行數比例
分支覆蓋率:if/else 等條件分支覆蓋
函式覆蓋率:呼叫過的函式比例
行覆蓋率:>80% 良好,>90% 優秀
 

工具

Python:pytest-cov → coverage report
JavaScript:Jest --coverage
Java:JaCoCo、Emma
 

測試金字塔原則

合理測試分配比例:

端到端測試:1
整合測試:3-5
單元測試:70-90
 

原因:單元測試快、穩定、成本低;端到端測試慢、不穩定、維護貴。

 

TDD(測試導向開發)工作流程

紅-綠-重構循環:

1. 紅(Red):寫失敗測試 → 定義需求
2. 綠(Green):寫剛好通過的程式碼
3. 重構(Refactor):優化程式碼,測試仍通過
 

# 1. 紅:寫失敗測試
def test_calculate_discount():
    assert calculate_discount(1000, 0.1) == 900  # 失敗

# 2. 綠:最小程式碼通過
def calculate_discount(price, rate):
    return price * (1 - rate)  # 通過

# 3. 重構:新增驗證、文件等
def calculate_discount(price, rate):
    """計算折扣價,rate 範圍 0-1"""
    if not 0 <= rate <= 1:
        raise ValueError("折扣率錯誤")
    return price * (1 - rate)
 

常見斷言類型

# 等值比較
assert result == expected
assert  result != expected

# 數值範圍
assert 900 <= result <= 1000
assert abs(result - expected) < 0.01  # 浮點數

# 例外處理
with pytest.raises(ValueError):
    risky_function()

# 容器內容
assert len(users) == 3
assert "admin" in roles
assert set(result) == set(expected)
 

CI/CD 整合範例

GitHub Actions

name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    - run: pip install pytest pytest-cov
    - run: pytest --cov=. --cov-report=xml
    - uses: codecov/codecov-action@v3
 

最佳實務總結

  • 一測試一斷言(Single Assertion)
  • 測試名稱描述行為:test_calculate_discount_for_vip_user
  • Mock 所有外部依賴(DB、API、檔案)
  • 覆蓋邊緣情況(0、空值、極端值)
  • 測試私有方法透過公開介面
  • 95%+ 覆蓋率目標
  • CI 強制通過才合併
     

名言:「未測試程式碼等於不存在程式碼」(Untested code is broken code)。

單元測試是專業開發者的標誌,從「寫了就行」到「測試先行」,代表程式思維的升級。它不僅捉蟲,更強制模組化設計、文件化行為、加速重構,是打造可靠軟體體系的基石。TDD + 高覆蓋率 = 生產力爆炸!