測驗:pytest 參數化測試
共 5 題,點選答案後會立即顯示結果
1. 參數化測試主要解決什麼問題?
2. 在 @pytest.mark.parametrize 中,參數名稱應該使用什麼格式?
@pytest.mark.parametrize(???, [(1, 2), (3, 4)])
3. 執行以下測試會產生幾個測試案例?
@pytest.mark.parametrize(“x”, [1, 2, 3])
def test_example(x):
assert x > 0
4. 如果想要讓測試結果更好讀,可以使用哪個參數自訂測試 ID?
5. 使用 pytest.param 搭配 marks=pytest.mark.xfail 的用途是什麼?
一句話說明
用一個測試函式跑多組資料,不用複製貼上寫很多個測試。
這篇會學到
讀完本篇後,你能夠:
- 理解參數化測試解決的問題(減少重複程式碼)
- 使用
@pytest.mark.parametrize裝飾器 - 設計多組測試資料
- 處理參數化測試的 ID 與可讀性
前置知識
- 會寫基本的 pytest 測試(
def test_xxx():+assert) - 知道什麼是 fixture
- 理解 Python 裝飾器的用法
問題:重複的測試程式碼
你可能會看到 AI 寫出這種程式碼:
def test_add_positive():
assert add(1, 2) == 3
def test_add_negative():
assert add(-1, -2) == -3
def test_add_zero():
assert add(0, 0) == 0
def test_add_mixed():
assert add(-1, 1) == 0
問題:四個測試做的事情一模一樣,只有輸入和預期結果不同。
翻譯:「這是在測試 add 函式的不同情況,但寫法很囉嗦」
解法:參數化測試
最小範例
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, -2, -3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
Code language: JavaScript (javascript)一句話:用一個測試函式,跑四組不同的資料。
逐行翻譯
@pytest.mark.parametrize("a, b, expected", [ # 定義參數名稱和測試資料
(1, 2, 3), # 第一組:a=1, b=2, expected=3
(-1, -2, -3), # 第二組:a=-1, b=-2, expected=-3
(0, 0, 0), # 第三組:a=0, b=0, expected=0
(-1, 1, 0), # 第四組:a=-1, b=1, expected=0
])
def test_add(a, b, expected): # 函式參數對應裝飾器定義的名稱
assert add(a, b) == expected # 每組資料跑一次這行
Code language: PHP (php)執行結果:
test_add[1-2-3] PASSED
test_add[-1--2--3] PASSED
test_add[0-0-0] PASSED
test_add[-1-1-0] PASSED
Code language: CSS (css)翻譯:「pytest 會自動把這一個測試展開成四個獨立的測試案例」
語法解析
@pytest.mark.parametrize 基本結構
@pytest.mark.parametrize("參數1, 參數2", [
(值1a, 值1b),
(值2a, 值2b),
])
def test_xxx(參數1, 參數2):
...
Code language: JavaScript (javascript)| 部分 | 意思 |
|---|---|
"參數1, 參數2" |
參數名稱,用逗號分隔(字串格式) |
[(值1a, 值1b), ...] |
測試資料列表,每個 tuple 是一組 |
def test_xxx(參數1, 參數2) |
函式參數要跟上面定義的對應 |
單一參數
如果只有一個參數,可以簡化:
@pytest.mark.parametrize("name", ["Alice", "Bob", "Charlie"])
def test_greet(name):
result = greet(name)
assert name in result
Code language: JavaScript (javascript)翻譯:「用三個不同的 name 跑 test_greet」
常見變化
AI 可能會這樣寫:
變化 1:測試多種輸入格式
@pytest.mark.parametrize("input_date, expected", [
("2024-01-15", "2024/01/15"),
("2024/01/15", "2024/01/15"),
("20240115", "2024/01/15"),
])
def test_normalize_date(input_date, expected):
assert normalize_date(input_date) == expected
Code language: JavaScript (javascript)翻譯:「測試日期正規化函式,確保各種輸入格式都能轉成統一格式」
變化 2:邊界值測試
@pytest.mark.parametrize("age, is_adult", [
(17, False), # 未滿 18
(18, True), # 剛好 18
(19, True), # 超過 18
(0, False), # 邊界:最小值
(150, True), # 邊界:極大值
])
def test_is_adult(age, is_adult):
assert check_adult(age) == is_adult
Code language: PHP (php)翻譯:「測試年齡判斷,特別測試邊界情況」
變化 3:測試錯誤情況
@pytest.mark.parametrize("invalid_input", [
"", # 空字串
None, # None
" ", # 只有空白
-1, # 負數
])
def test_validate_rejects_invalid(invalid_input):
with pytest.raises(ValueError):
validate(invalid_input)
Code language: PHP (php)翻譯:「確保這些無效輸入都會拋出 ValueError」
進階用法
自訂測試 ID
預設的測試 ID 是用參數值組成,有時候不好讀:
test_add[-1--2--3] PASSED # 參數是 -1, -2, -3,但看起來很亂
Code language: CSS (css)用 ids 參數自訂:
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(-1, -2, -3),
(0, 0, 0),
], ids=["positive", "negative", "zero"])
def test_add(a, b, expected):
assert add(a, b) == expected
Code language: JavaScript (javascript)執行結果變成:
test_add[positive] PASSED
test_add[negative] PASSED
test_add[zero] PASSED
Code language: CSS (css)翻譯:「給每組測試資料取個好讀的名字」
用 pytest.param 標記特殊情況
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
pytest.param(-1, -2, -3, id="negative"), # 自訂 ID
pytest.param(0, 0, 1, marks=pytest.mark.xfail), # 預期會失敗
pytest.param(999, 999, 0, marks=pytest.mark.skip), # 跳過這組
])
def test_add(a, b, expected):
assert add(a, b) == expected
Code language: PHP (php)| 用法 | 意思 |
|---|---|
pytest.param(..., id="xxx") |
自訂這組的 ID |
marks=pytest.mark.xfail |
標記「這組預期會失敗」 |
marks=pytest.mark.skip |
跳過這組不測 |
參數化 + Fixture 結合
範例:用不同設定測試
@pytest.fixture
def client():
return APIClient()
@pytest.mark.parametrize("endpoint", ["/users", "/orders", "/products"])
def test_endpoints_return_200(client, endpoint):
response = client.get(endpoint)
assert response.status_code == 200
Code language: JavaScript (javascript)翻譯:「用同一個 client(來自 fixture),測試三個不同的 endpoint」
範例:Fixture 參數化
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
db = create_database(request.param)
yield db
db.cleanup()
def test_save_user(database):
database.save(User(name="Alice"))
assert database.get("Alice") is not None
Code language: JavaScript (javascript)翻譯:「用三種不同的資料庫跑同一個測試」
實際案例
案例 1:API 回應驗證
@pytest.mark.parametrize("status_code, should_retry", [
(200, False), # 成功,不重試
(201, False), # 成功,不重試
(400, False), # 客戶端錯誤,不重試
(500, True), # 伺服器錯誤,要重試
(502, True), # 閘道錯誤,要重試
(503, True), # 服務不可用,要重試
])
def test_should_retry(status_code, should_retry):
assert check_should_retry(status_code) == should_retry
Code language: PHP (php)案例 2:密碼強度檢查
@pytest.mark.parametrize("password, is_strong", [
("abc", False), # 太短
("abcdefgh", False), # 沒有數字
("12345678", False), # 沒有字母
("Abc12345", True), # OK
("Abc12345!@#", True), # OK,有特殊符號更好
], ids=["too_short", "no_number", "no_letter", "valid", "valid_with_symbols"])
def test_password_strength(password, is_strong):
assert validate_password_strength(password) == is_strong
Code language: PHP (php)案例 3:檔案格式解析
@pytest.mark.parametrize("filename, expected_type", [
("data.json", "json"),
("data.JSON", "json"), # 大寫也要能處理
("config.yaml", "yaml"),
("config.yml", "yaml"), # yml 和 yaml 都是 YAML
("script.py", "python"),
("unknown.xyz", None), # 不認識的格式
])
def test_detect_file_type(filename, expected_type):
assert detect_file_type(filename) == expected_type
Code language: PHP (php)Vibe Coder 檢查點
看到 @pytest.mark.parametrize 時確認:
- [ ] 參數名稱跟函式參數對得上嗎?
- [ ] 測試資料有涵蓋邊界情況嗎?(0、負數、空值、極大值)
- [ ] 測試 ID 好讀嗎?如果不好讀,有沒有加
ids參數? - [ ] 每組測試資料的 tuple 長度是否一致?
- [ ] 有沒有測試「應該失敗」的情況?
常見錯誤
錯誤 1:參數名稱不匹配
# 錯誤:裝飾器寫 a, b,函式寫 x, y
@pytest.mark.parametrize("a, b", [(1, 2)])
def test_add(x, y): # 應該是 a, b
assert x + y == 3
Code language: PHP (php)錯誤 2:tuple 長度不一致
# 錯誤:有的 3 個值,有的 2 個值
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(4, 5), # 少了 expected!
])
def test_add(a, b, expected):
assert a + b == expected
Code language: PHP (php)錯誤 3:忘記是字串格式
# 錯誤:參數名稱要用引號包起來
@pytest.mark.parametrize(a, b, [(1, 2)]) # 應該是 "a, b"
def test_add(a, b):
...
Code language: PHP (php)重點整理
| 概念 | 一句話 |
|---|---|
@pytest.mark.parametrize |
用一個測試跑多組資料 |
"a, b" |
參數名稱,字串格式用逗號分隔 |
[(1, 2), (3, 4)] |
測試資料列表 |
ids=["case1", "case2"] |
自訂測試 ID |
pytest.param(..., marks=...) |
標記特殊情況(xfail, skip) |
延伸:知道就好
這些進階功能遇到再查:
- 多重參數化:同時用多個
@pytest.mark.parametrize做組合測試 - indirect 參數:把參數傳給 fixture 而不是測試函式
- 動態產生測試資料:用
pytestgeneratetests鉤子函式
進階測驗:pytest 參數化測試
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你需要測試一個密碼驗證函式,需要確保它能正確處理:太短的密碼、沒有數字的密碼、沒有字母的密碼、以及合法的密碼。最適合的測試方式是? 情境題
2. 同事寫了以下測試,但執行時出現錯誤。問題出在哪裡? 錯誤診斷
@pytest.mark.parametrize(“a, b, expected”, [
(1, 2, 3),
(4, 5),
(0, 0, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
3. 你正在測試 API endpoint,需要用同一個 client 物件測試三個不同的路徑:/users、/orders、/products。最佳的實作方式是? 情境題
4. 執行測試後看到以下輸出,哪個做法可以讓測試結果更容易閱讀? 錯誤診斷
test_add[-1–2–3] PASSED
test_add[0-0-0] PASSED
test_add[100-200-300] PASSED
5. 你的專案有一個已知的 bug,某個函式在輸入 0 時會回傳錯誤結果,但暫時無法修復。你想讓測試能繼續執行其他案例,同時記錄這個問題。應該怎麼做? 情境題
@pytest.mark.parametrize(“value, expected”, [
(1, 2),
(0, 0), # 這個案例會失敗
(5, 10),
])
def test_double(value, expected):
assert double(value) == expected