跳轉至

建立 2026-10-11 更新 2026-10-11

參數化測試

參數化測試(parametrized test)是用 @pytest.mark.parametrize 讓同一個測試函式,以多組不同的輸入與預期值各執行一次。它能把十幾個長得幾乎一樣的測試濃縮成一個,而且每組資料仍會獨立顯示通過或失敗。

基本用法

不使用參數化時,測試多組資料只能一直複製:

def test_discount_20():
    assert apply_discount(100, 20) == 80

def test_discount_0():
    assert apply_discount(100, 0) == 100

改用參數化:

import pytest
from shop import apply_discount


@pytest.mark.parametrize(
    ("price", "percent", "expected"),
    [
        (100, 20, 80),
        (100, 0, 100),
        (100, 100, 0),
        (59.9, 10, 53.91),
    ],
)
def test_apply_discount(price, percent, expected):
    assert apply_discount(price, percent) == expected

第一個參數列出參數名稱,第二個是資料清單,每個 tuple 是一組。執行結果會分成四個測試:

test_shop.py::test_apply_discount[100-20-80] PASSED
test_shop.py::test_apply_discount[100-0-100] PASSED
test_shop.py::test_apply_discount[100-100-0] PASSED
test_shop.py::test_apply_discount[59.9-10-53.91] PASSED

方括號裡是自動產生的測試 ID,失敗時一眼就知道是哪一組資料出錯。

讓測試 ID 更好讀

資料較複雜時,自動產生的 ID 不易閱讀。用 pytest.param(..., id=...) 為每組資料命名:

@pytest.mark.parametrize(
    ("price", "percent", "expected"),
    [
        pytest.param(100, 20, 80, id="normal"),
        pytest.param(100, 0, 100, id="no-discount"),
        pytest.param(100, 100, 0, id="free"),
    ],
)
def test_apply_discount(price, percent, expected):
    assert apply_discount(price, percent) == expected

有了 ID,輸出會變成 test_apply_discount[free],也可以用 pytest -k free 只跑那一組。

測試 ID 請用英文

pytest 預設會把 ID 中的非 ASCII 字元跳脫,id="免費" 會顯示成 \u514d\u8cbb,也無法用 -k 免費 選取。因此 ID 建議使用英文、數字與連字號。

參數化錯誤輸入

正常輸入與錯誤輸入通常分成兩個測試,各自參數化,比在一個測試裡用 if 判斷清楚:

@pytest.mark.parametrize("percent", [-1, 101, 1000])
def test_invalid_percent(percent):
    with pytest.raises(ValueError):
        apply_discount(100, percent)

個別資料加上 marker

已知某組資料目前會失敗(例如等待修正的 bug),可以只對那一組加上 xfail,不影響其他資料:

@pytest.mark.parametrize(
    ("text", "expected"),
    [
        ("hello", "HELLO"),
        pytest.param("ß", "SS", marks=pytest.mark.xfail(reason="尚未處理德文字元")),
    ],
)
def test_upper(text, expected):
    assert my_upper(text) == expected

xfail 的意義見 Marker。

疊加參數化:組合測試

疊加多個 parametrize 時,pytest 會產生所有組合(笛卡兒積)。下例會執行 2 × 3 = 6 個測試:

@pytest.mark.parametrize("currency", ["TWD", "USD"])
@pytest.mark.parametrize("amount", [0, 1, 999])
def test_format_price(currency, amount):
    assert format_price(amount, currency).startswith(currency)

組合數會快速成長,只在每個組合都有測試價值時使用。

參數化 fixture

Fixture 也能參數化,讓所有使用它的測試都對每個參數各跑一次,適合「同一套測試要對多種資料庫或設定執行」的情境:

@pytest.fixture(params=["sqlite", "postgres"])
def db_url(request):
    return f"{request.param}://test"


def test_connect(db_url):
    assert "://" in db_url   # 會執行兩次

request 是 pytest 內建的 fixture,request.param 取得目前這一輪的參數值。

推薦影音

pytest's parametrize

pytest 核心開發者 Anthony Sottile 約 12 分鐘講解 parametrize 的用法:如何用一個測試函式涵蓋多組輸入,適合讀完本頁「基本用法」後觀看。

Multiply your Testing Effectiveness with Parameterized Testing(PyCon US 2020)

Brian Okken 在 PyCon US 的演講,從一個簡單測試出發,比較函式參數化、fixture 參數化與 pytest_generate_tests 三種做法,並介紹測試 ID 與用產生器(generator)提供資料。