昨天我們學會了測試生命週期,解決了測試污染的問題。但現在面對一個新的挑戰:「要測試同一個函數的多組輸入輸出,難道要寫幾十個類似的測試嗎?」
想像一個場景:你要為數學工具庫的 is_prime
函數寫測試,需要驗證很多數字。如果每組都寫一個獨立的測試,程式碼會變得非常冗長且難以維護。今天我們要學習「參數化測試」,用優雅的方式處理大量測試資料。
今天結束後,你將學會:
pytest.mark.parametrize
的使用第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
參數化測試(Parameterized Testing)是一種測試技術,讓你能用同一組測試邏輯驗證多組不同的輸入資料。它有幾個別名:
傳統方式需要為每個輸入寫一個獨立的測試,產生大量重複程式碼。參數化測試可以用一個測試函數處理多組資料:
# ✅ 參數化測試:乾淨、簡潔
@pytest.mark.parametrize("input,expected", [
(2, True),
(3, True),
(5, True),
(4, False),
(6, False),
])
def test_is_prime(input, expected):
assert is_prime(input) == expected
pytest 提供了 @pytest.mark.parametrize
裝飾器來實作參數化測試:
# 基本語法
@pytest.mark.parametrize("param1,param2", [
(value1a, value1b),
(value2a, value2b),
(value3a, value3b),
])
def test_something(param1, param2):
# 測試邏輯
pass
# 使用變數名稱
@pytest.mark.parametrize("input,expected", [
(10, 100),
(20, 400),
(30, 900),
])
def test_square(input, expected):
assert square(input) == expected
建立 src/math/prime_checker.py
:
def is_prime(n: int) -> bool:
"""檢查一個數字是否為質數"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(n ** 0.5) + 1, 2):
if n % i == 0:
return False
return True
建立 tests/day06/test_prime_checker.py
:
import pytest
from src.math.prime_checker import is_prime
class TestPrimeChecker:
@pytest.mark.parametrize("input,expected", [
(2, True), (3, True), (5, True), (7, True),
(4, False), (6, False), (8, False), (9, False),
])
def test_basic_prime_numbers(self, input, expected):
assert is_prime(input) == expected
@pytest.mark.parametrize("input,expected", [
(0, False), (1, False), (-1, False),
])
def test_edge_cases(self, input, expected):
assert is_prime(input) == expected
@pytest.mark.parametrize("input,expected", [
(97, True),
(101, True),
(100, False),
])
def test_large_numbers(self, input, expected):
assert is_prime(input) == expected
當測試資料變複雜時,可以使用 pytest.param
加上描述:
@pytest.mark.parametrize("email,expected", [
pytest.param("user@example.com", True, id="valid_email"),
pytest.param("invalid-email", False, id="no_at_symbol"),
pytest.param("@example.com", False, id="no_username"),
pytest.param("user@", False, id="no_domain"),
pytest.param("", False, id="empty_string"),
])
def test_email_validation(email, expected):
assert is_valid_email(email) == expected
建立 src/math/calculator.py
:
class Calculator:
def add(self, a: float, b: float) -> float:
return a + b
def subtract(self, a: float, b: float) -> float:
return a - b
def multiply(self, a: float, b: float) -> float:
return a * b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("Division by zero")
return a / b
建立 tests/day06/test_calculator.py
:
import pytest
from src.math.calculator import Calculator
class TestCalculator:
def setup_method(self):
self.calculator = Calculator()
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(5, 3, 8),
(-1, 1, 0),
])
def test_addition(self, a, b, expected):
assert self.calculator.add(a, b) == expected
@pytest.mark.parametrize("a,b,expected", [
(5, 3, 2),
(10, 5, 5),
(0, 5, -5),
])
def test_subtraction(self, a, b, expected):
assert self.calculator.subtract(a, b) == expected
@pytest.mark.parametrize("a,b", [
(5, 0),
(10, 0),
(-5, 0),
])
def test_division_by_zero_raises_error(self, a, b):
with pytest.raises(ValueError, match="Division by zero"):
self.calculator.divide(a, b)
設計測試資料時要考慮:
建立 src/utils/string_utils.py
:
def capitalize(text: str) -> str:
"""將字串首字母大寫"""
if not text:
return ''
return text[0].upper() + text[1:].lower()
def truncate(text: str, max_length: int) -> str:
"""截斷字串至指定長度"""
if len(text) <= max_length:
return text
return text[:max_length - 3] + '...'
建立 tests/day06/test_string_utils.py
:
import pytest
from src.utils.string_utils import capitalize, truncate
@pytest.mark.parametrize("input_text,expected", [
("hello", "Hello"),
("WORLD", "World"),
("typescript", "Typescript"),
("", ""),
])
def test_capitalize(input_text, expected):
assert capitalize(input_text) == expected
@pytest.mark.parametrize("text,max_len,expected", [
("hello world", 5, "he..."),
("short", 10, "short"),
])
def test_truncate(text, max_len, expected):
assert truncate(text, max_len) == expected
# ❌ 錯誤:描述不清、邏輯混雜
@pytest.mark.parametrize("op,a,b,expected", [
('add', 1, 2, 3),
('subtract', 5, 2, 3),
])
def test_operations(op, a, b, expected):
# 複雜的條件邏輯
pass
# ✅ 正確:清楚描述、單一職責
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(4, 5, 9),
])
def test_addition(a, b, expected):
assert add(a, b) == expected
今天我們深入學習了參數化測試的概念和實際應用:
參數化測試是提高測試效率和覆蓋率的強力工具:
記住:好的參數化測試資料設計是測試品質的關鍵。
明天我們將學習「測試替身基礎」,了解如何使用 Mock、Stub 等技術來隔離測試對象,讓測試更專注和可靠。