單元測試(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 + 高覆蓋率 = 生產力爆炸!