如何參數化固定裝置和測試函數

pytest 能在多個層級參數化測試

@pytest.mark.parametrize:參數化測試函數

內建的pytest.mark.parametrize 裝飾器能為測試函數參數化參數。以下是實作檢查特定輸入是否會導致預期輸出的測試函數的典型範例

# content of test_expectation.py
import pytest


@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expected

在此,@parametrize 裝飾器定義三個不同的(test_input,expected) 元組,因此test_eval 函數將會使用它們輪流執行三次

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_expectation.py ..F                                              [100%]

================================= FAILURES =================================
____________________________ test_eval[6*9-42] _____________________________

test_input = '6*9', expected = 42

    @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
    def test_eval(test_input, expected):
>       assert eval(test_input) == expected
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

test_expectation.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54...
======================= 1 failed, 2 passed in 0.12s ========================

注意

參數值會原樣傳遞給測試(完全不複製)。

例如,如果你傳遞清單或字典作為參數值,而且測試案例程式碼對其進行變異,變異將會反映在後續的測試案例呼叫中。

注意

pytest 預設會跳脫參數化中 unicode 字串使用的所有非 ASCII 字元,因為它有幾個缺點。然而,如果你想在參數化中使用 unicode 字串,並在終端機中看到它們原本的樣子(未跳脫),請在pytest.ini 中使用這個選項

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True

但請記住,這可能會造成不必要的副作用,甚至會產生錯誤,具體取決於所使用的作業系統和目前安裝的外掛程式,因此請自行承擔風險。

如本範例中所設計,只有一組輸入/輸出值無法通過簡單的測試函式。而且與測試函式引數一樣,你可以在追蹤中看到 inputoutput 值。

請注意,你也可以對類別或模組使用 parametrize 標記(請參閱 如何使用屬性標記測試函式),這會呼叫多個具有引數集的函式,例如

import pytest


@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

若要將模組中的所有測試參數化,你可以指定給 pytestmark 全域變數

import pytest

pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])


class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected

也可以在 parametrize 中標記個別測試實例,例如使用內建 mark.xfail

# content of test_expectation.py
import pytest


@pytest.mark.parametrize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected

讓我們執行此操作

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_expectation.py ..x                                              [100%]

======================= 2 passed, 1 xfailed in 0.12s =======================

先前導致失敗的一個參數集現在顯示為「xfailed」(預期會失敗)測試。

如果提供給 parametrize 的值產生一個空清單 - 例如,如果它們是由某些函式動態產生的 - 則 pytest 的行為由 empty_parameter_set_mark 選項定義。

若要取得多個參數化引數的所有組合,你可以堆疊 parametrize 裝飾器

import pytest


@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass

這將執行測試,其中引數設定為 x=0/y=2x=1/y=2x=0/y=3x=1/y=3,以裝飾器的順序用盡參數。

基本 pytest_generate_tests 範例

有時候你可能想要實作自己的參數化方案,或實作一些動態機制來決定固定裝置的參數或範圍。為此,你可以使用 pytest_generate_tests 鉤子,它會在收集測試函式時呼叫。透過傳入的 metafunc 物件,你可以檢查請求的測試內容,最重要的是,你可以呼叫 metafunc.parametrize() 來造成參數化。

例如,假設我們想要執行一個測試,採用我們想要透過新的 pytest 命令列選項設定的字串輸入。我們先撰寫一個簡單的測試,接受 stringinput 固定裝置函式引數

# content of test_strings.py


def test_valid_string(stringinput):
    assert stringinput.isalpha()

現在我們新增一個 conftest.py 檔案,其中包含新增命令列選項和我們測試函式的參數化

# content of conftest.py


def pytest_addoption(parser):
    parser.addoption(
        "--stringinput",
        action="append",
        default=[],
        help="list of stringinputs to pass to test functions",
    )


def pytest_generate_tests(metafunc):
    if "stringinput" in metafunc.fixturenames:
        metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))

如果我們現在傳遞兩個 stringinput 值,我們的測試將執行兩次

$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
..                                                                   [100%]
2 passed in 0.12s

我們也用會導致測試失敗的字串輸入來執行

$ pytest -q --stringinput="!" test_strings.py
F                                                                    [100%]
================================= FAILURES =================================
___________________________ test_valid_string[!] ___________________________

stringinput = '!'

    def test_valid_string(stringinput):
>       assert stringinput.isalpha()
E       AssertionError: assert False
E        +  where False = <built-in method isalpha of str object at 0xdeadbeef0001>()
E        +    where <built-in method isalpha of str object at 0xdeadbeef0001> = '!'.isalpha

test_strings.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_strings.py::test_valid_string[!] - AssertionError: assert False
1 failed in 0.12s

正如預期,我們的測試函數失敗。

如果您未指定字串輸入,它將被略過,因為 metafunc.parametrize() 將使用空參數清單呼叫

$ pytest -q -rs test_strings.py
s                                                                    [100%]
========================= short test summary info ==========================
SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at /home/sweet/project/test_strings.py:2
1 skipped in 0.12s

請注意,當使用不同的參數集多次呼叫 metafunc.parametrize 時,這些集合中的所有參數名稱不能重複,否則會引發錯誤。

更多範例

如需更多範例,您可能想查看 更多參數化範例