如何參數化 fixtures 和測試函數

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 範例

有時您可能想要實作自己的參數化方案,或實作一些動態性來確定 fixture 的參數或範圍。為此,您可以使用 pytest_generate_tests hook,它在收集測試函數時被調用。透過傳遞進來的 metafunc 物件,您可以檢查請求測試上下文,最重要的是,您可以調用 metafunc.parametrize() 來引起參數化。

例如,假設我們想要運行一個接受字串輸入的測試,我們想要透過新的 pytest 命令列選項設定這些輸入。讓我們先編寫一個簡單的測試,接受 stringinput fixture 函數參數

# 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

讓我們也使用一個將導致測試失敗的 stringinput 運行

$ 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

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

如果您未指定 stringinput,它將被跳過,因為 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 並使用不同的參數集時,這些集合中的所有參數名稱都不能重複,否則將引發錯誤。

更多範例

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