自动化测试框架pytest教程16-高级参数化
使用复杂的值
有时你可能想用数据结构或对象作为参数化的值。
ch16/test_ids.py
@pytest.mark.parametrize(
"starting_card",
[
Card("foo", state="todo"),
Card("foo", state="in prog"),
Card("foo", state="done"),
],
)
def test_card(cards_db, starting_card):
index = cards_db.add_card(starting_card)
cards_db.finish(index)
card = cards_db.get_card(index)
assert card.state == "done"
- 执行
$ pytest -v test_ids.py::test_card
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=3603486280
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... collected 3 items
test_ids.py::test_card[starting_card1] PASSED [ 33%]
test_ids.py::test_card[starting_card2] PASSED [ 66%]
test_ids.py::test_card[starting_card0] PASSED [100%]
============================== 3 passed in 0.22s ==============================
对于那些没有明显字符串值的对象,pytest会对其进行编号。"starting_card0","starting_card1",等等。
创建自定义标识符
你可以通过使用ids参数定义函数来生成标识符。通常情况下,内置的str或repr函数可以正常工作。
让我们尝试使用str作为一个ID函数。
ch16/test_ids.py
card_list = [
Card("foo", state="todo"),
Card("foo", state="in prog"),
Card("foo", state="done"),
]
@pytest.mark.parametrize("starting_card", card_list, ids=str)
def test_id_str(cards_db, starting_card):
...
index = cards_db.add_card(starting_card)
cards_db.finish(index)
card = cards_db.get_card(index)
assert card.state == "done"
这里我们添加了ids=str。
- 执行
$ pytest -v test_ids.py::test_id_str
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=3244471004
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... collected 3 items
test_ids.py::test_id_str[Card(summary='foo', owner=None, state='done', id=None)] PASSED [ 33%]
test_ids.py::test_id_str[Card(summary='foo', owner=None, state='in prog', id=None)] PASSED [ 66%]
test_ids.py::test_id_str[Card(summary='foo', owner=None, state='todo', id=None)] PASSED [100%]
============================== 3 passed in 0.19s ==============================
让我们来定义我们自己的ID函数。它需要接收Card对象并返回字符串。而我们将把id设置为我们的新函数。
ch16/test_ids.py
def card_state(card):
return card.state
@pytest.mark.parametrize("starting_card", card_list, ids=card_state)
def test_id_func(cards_db, starting_card):
...
index = cards_db.add_card(starting_card)
cards_db.finish(index)
card = cards_db.get_card(index)
assert card.state == "done"
- 执行
$ pytest -v test_ids.py::test_id_func
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=1314635729
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... collected 3 items
test_ids.py::test_id_func[done] PASSED [ 33%]
test_ids.py::test_id_func[in prog] PASSED [ 66%]
test_ids.py::test_id_func[todo] PASSED [100%]
============================== 3 passed in 0.18s ==============================
许多ID函数会很短。如果是单行函数,用lambda函数效果很好。
ch16/test_ids.py
@pytest.mark.parametrize(
"starting_card", card_list, ids=lambda c: c.state
)
def test_id_lambda(cards_db, starting_card):
...
index = cards_db.add_card(starting_card)
cards_db.finish(index)
card = cards_db.get_card(index)
assert card.state == "done"
输出将看起来是一样的。
ids功能在参数化的固定程序和pytest_generate_tests中也是可用的。还有两种方法可以创建自定义标识符:pytest.param和id列表。
在pytest.param中添加一个ID
在标记文件、类和参数中,我们用pytest.param为参数化的值添加标记。pytest.param也可以用来添加ID。在下面的例子中,我们将为一个参数添加一个 "特殊 "的ID。
ch16/test_ids.py
c_list = [
Card("foo", state="todo")。
" pytest.param(Card("foo", state="in prog"), id="special") 。
Card("foo", state="done"),
]
@pytest.mark.parametrize("starting_card", c_list, ids=card_state)
def test_id_param(card_db, starting_card):
...
这个方法在与其他方法结合时特别有用。在这个例子中,我们用pytest.param指定了一个 "特殊 "的ID,并让ids=cards_state()生成其他的ID。
结果测试运行看起来像这样。
$ pytest -v test_ids.py::test_id_param
========================= 测试会话开始 ==========================
收集了3个项目
test_ids.py::test_id_param[todo] PASSED [ 33%]
test_ids.py::test_id_param[special] PASSED [ 66%]
test_ids.py::test_id_param[done] PASSED [100%]
========================== 3在0.02s内通过 ===========================
如果你只有一两个需要特殊处理的ID,使用pytest.param是非常好的。如果你想手工编写所有的ID,pytest.param会很麻烦。如果你想为所有的值编写自定义的ID,使用一个列表可能更容易维护。
你可以给ids提供一个列表,而不是函数。
ch16/test_ids.py
id_list = ["todo", "in prog", "done"]
@pytest.mark.parametrize("starting_card", card_list, ids=id_list)
def test_id_list(cards_db, starting_card):
...
index = cards_db.add_card(starting_card)
cards_db.finish(index)
card = cards_db.get_card(index)
assert card.state == "done"
你必须格外小心,以保持列表的同步。否则,ID就会出错。保持ID和值在一起的一个方法是使用ID作为字典的键。然后你可以用 .keys() 作为 ID 的列表,用 .values() 作为参数的列表。当ID不容易用函数生成时,以这种方式使用字典特别有用。
text_variants = {
"Short": "x",
"With Spaces": "x y z",
"End In Spaces": "x ",
"Mixed Case": "SuMmArY wItH MiXeD cAsE",
"Unicode": "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾",
"Newlines": "a\nb\nc",
"Tabs": "a\tb\tc",
}
@pytest.mark.parametrize(
"variant", text_variants.values(), ids=text_variants.keys()
)
def test_summary_variants(cards_db, variant):
i = cards_db.add_card(Card(summary=variant))
c = cards_db.get_card(i)
assert c.summary == variant
使用动态值
ch16/test_param_gen.py
import pytest
from cards import Card
def text_variants():
variants = {
"Short": "x",
"With Spaces": "x y z",
"End in Spaces": "x ",
"Mixed Case": "SuMmArY wItH MiXeD cAsE",
"Unicode": "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾",
"Newlines": "a\nb\nc",
"Tabs": "a\tb\tc",
}
for key, value in variants.items():
yield pytest.param(value, id=key)
@pytest.mark.parametrize("variant", text_variants())
def test_summary(cards_db, variant):
i = cards_db.add_card(Card(summary=variant))
c = cards_db.get_card(i)
assert c.summary == variant
- 执行
$ pytest test_param_gen.py -v
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=1784156481
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... collected 7 items
test_param_gen.py::test_summary[Mixed Case] PASSED [ 14%]
test_param_gen.py::test_summary[Tabs] PASSED [ 28%]
test_param_gen.py::test_summary[Newlines] PASSED [ 42%]
test_param_gen.py::test_summary[End in Spaces] PASSED [ 57%]
test_param_gen.py::test_summary[Short] PASSED [ 71%]
test_param_gen.py::test_summary[Unicode] PASSED [ 85%]
test_param_gen.py::test_summary[With Spaces] PASSED [100%]
============================== 7 passed in 0.41s ==============================
使用多个参数
ch16/test_multiple.py
from cards import Card
summaries = ["short", "a bit longer"]
owners = ["First", "First M. Last"]
states = ["todo", "in prog", "done"]
@pytest.mark.parametrize(
"summary, owner, state",
[
("short", "First", "todo"),
("short", "First", "in prog"),
# ...
],
)
def test_add_lots(cards_db, summary, owner, state):
"""Make sure adding to db doesn't change values."""
i = cards_db.add_card(Card(summary, owner=owner, state=state))
card = cards_db.get_card(i)
expected = Card(summary, owner=owner, state=state)
assert card == expected
@pytest.mark.parametrize("state", states)
@pytest.mark.parametrize("owner", owners)
@pytest.mark.parametrize("summary", summaries)
def test_stacking(cards_db, summary, owner, state):
"""Make sure adding to db doesn't change values."""
...
expected = Card(summary, owner=owner, state=state)
i = cards_db.add_card(Card(summary, owner=owner, state=state))
card = cards_db.get_card(i)
assert card == expected
- 执行
$ pytest test_multiple.py::test_add_lots -v
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=3994018912
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... collected 2 items
test_multiple.py::test_add_lots[short-First-in prog] PASSED [ 50%]
test_multiple.py::test_add_lots[short-First-todo] PASSED [100%]
============================== 2 passed in 0.15s ==============================
$ pytest test test_multiple.py::test_stacking -v
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
Using --randomly-seed=1036957166
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0, randomly-3.12.0, repeat-0.9.1, xdist-3.1.0
collecting ... ERROR: file or directory not found: test
collected 0 items
============================ no tests ran in 0.05s ============================