自动化测试框架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 ============================

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容