自动化测试框架pytest教程5-参数化(数据驱动)

我们将研究如何把一个测试函数变成许多测试用例,以更少的工作量进行更全面的测试。我们将用参数化来做这件事。

参数化测试是指向我们的测试函数添加参数,并向测试传入多组参数,以创建新的测试案例。我们将看看在pytest中实现参数化测试的三种方法,并按照它们的顺序进行选择。

  • 参数化函数
  • 参数化Fixture
  • pytest_generate_tests钩子函数

减少代码的尝试

让我们为finish()API方法写一些测试。

cards_proj/src/cards/api.py

    def finish(self, card_id: int):
        """Set a card state to 'done'."""
        self.update_card(card_id, Card(state="done"))

应用程序中使用的状态是 "todo"、"in progressive "和 "done",这个方法将一个卡片的状态设置为 "done"。

Card的起始状态。它可能是 "todo"、"progressive",甚至是已经 "完成"。

ch5/test_finish.py

from cards import Card


def test_finish_from_in_prog(cards_db):
    index = cards_db.add_card(Card("second edition", state="in prog"))
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"


def test_finish_from_done(cards_db):
    index = cards_db.add_card(Card("write a book", state="done"))
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"


def test_finish_from_todo(cards_db):
    index = cards_db.add_card(Card("create a course", state="todo"))
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"

让我们来运行它

$ cd /path/to/code/ch5
$ pytest -v test_finish.py
$ pytest -v test_finish.py
============================= 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
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
collecting ... collected 3 items

test_finish.py::test_finish_from_in_prog PASSED                          [ 33%]
test_finish.py::test_finish_from_done PASSED                             [ 66%]
test_finish.py::test_finish_from_todo PASSED                             [100%]

============================== 3 passed in 0.18s ==============================

一种减少代码的方法

ch5/test_finish_combined.py

from cards import Card


def test_finish(cards_db):
    for c in [
        Card("write a book", state="done"),
        Card("second edition", state="in prog"),
        Card("create a course", state="todo"),
    ]:
        index = cards_db.add_card(c)
        cards_db.finish(index)
        card = cards_db.get_card(index)
        assert card.state == "done"

执行:

$ pytest test_finish_combined.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
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
collected 1 item

test_finish_combined.py .                                                [100%]

============================== 1 passed in 0.14s ==============================

我们已经消除了多余的代码。但是还有其他问题。我们只有一个测试用例报告,而不是三个。

如果其中一个测试用例失败了,如果不看回溯或其他一些调试信息,我们真的不知道是哪一个。如果其中一个测试用例失败了,那么失败后的测试用例将不会被运行。 当一个断言失败时,pytest停止运行测试。

参数化函数

要对测试函数进行参数化处理,需要在测试定义中添加参数,并使用@pytest.mark.parametrize()装饰器来定义要传递给测试的参数集,像这样。

ch5/test_func_param.py

import pytest
from cards import Card


@pytest.mark.parametrize(
    "start_summary, start_state",
    [
        ("write a book", "done"),
        ("second edition", "in prog"),
        ("create a course", "todo"),
    ],
)
def test_finish(cards_db, start_summary, start_state):
    initial_card = Card(summary=start_summary, state=start_state)
    index = cards_db.add_card(initial_card)

    cards_db.finish(index)

    card = cards_db.get_card(index)
    assert card.state == "done"

@pytest.mark.parametrize("start_state", ["done", "in prog", "todo"])
def test_finish_simple(cards_db, start_state):
    c = Card("write a book", state=start_state)
    index = cards_db.add_card(c)
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"
    

test_finish()函数现在有它原来的card_db Fixture作为参数,但也有两个新参数:start_summary和start_state。这些参数直接与@pytest.mark.parametrize()的第一个参数匹配。

@pytest.mark.parametrize()的第一个参数是一个参数名称的列表。它们是字符串,可以是一个实际的字符串列表,如["start_summary", "start_state"],也可以是一个逗号分隔的字符串,如 "start_summary, start_state"。@pytest.mark.parametrize()的第二个参数是我们的测试案例的列表。列表中的每个元素都是一个测试用例,由一个元组或列表表示,每个参数都有一个元素被发送到测试函数中。

pytest将为每个(start_summary, start_state)对运行一次测试,并将每个测试作为一个单独的测试报告。

$ pytest -v test_func_param.py::test_finish
============================= 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
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
collecting ... collected 3 items

test_func_param.py::test_finish[write a book-done] PASSED                [ 33%]
test_func_param.py::test_finish[second edition-in prog] PASSED           [ 66%]
test_func_param.py::test_finish[create a course-todo] PASSED             [100%]

============================== 3 passed in 0.14s ==============================

$ pytest -v test_func_param.py::test_finish_simple
============================= 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
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
collecting ... collected 3 items

test_func_param.py::test_finish_simple[done] PASSED                      [ 33%]
test_func_param.py::test_finish_simple[in prog] PASSED                   [ 66%]
test_func_param.py::test_finish_simple[todo] PASSED                      [100%]

============================== 3 passed in 0.15s ==============================

对Fixture进行参数化

使用函数参数化时,pytest对我们提供的每组参数值都调用一次测试函数。使用Fixture参数化,我们将这些参数转移到Fixture中,然后pytest将为我们提供的每组值调用一次Fixture。然后每个依赖于夹具的测试函数都会被调用,每个Fixture的值都会被调用。

另外,语法是不同的。

ch5/test_fix_param.py

import pytest
from cards import Card


@pytest.fixture(params=["done", "in prog", "todo"])
def start_state(request):
    return request.param


def test_finish(cards_db, start_state):
    c = Card("write a book", state=start_state)
    index = cards_db.add_card(c)
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"

执行

import pytest
from cards import Card


@pytest.fixture(params=["done", "in prog", "todo"])
def start_state(request):
    return request.param


def test_finish(cards_db, start_state):
    c = Card("write a book", state=start_state)
    index = cards_db.add_card(c)
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"

乍一看,夹具参数化的作用与函数参数化的作用差不多,只是代码多了一点。有的时候,Fixture参数化是有好处的。

Fixture参数化的好处是可以为每组参数运行Fixture。如果你有需要为每个测试案例运行的设置或拆除代码,这很有用--可能是不同的数据库连接,或不同的文件内容,或其他。

它还有一个好处,就是许多测试函数能够以相同的参数集运行。所有使用start_state夹具的测试都会被调用三次,每个启动状态一次。

Fixture参数化也是思考同一问题的不同方式。即使在测试finish()的情况下,如果我从 "相同的测试,不同的数据 "的角度来考虑,我经常倾向于函数参数化。但如果我考虑的是 "同样的测试,不同的开始状态",我就会倾向于Fixture参数化。

pytest_generate_tests参数化

参数化的第三种方法是使用pytest_generate_tests的钩子函数。钩子函数经常被插件用来改变pytest的正常操作流程

用pytest_generate_tests实现与之前相同的流程:

ch5/test_gen.py

from cards import Card


def pytest_generate_tests(metafunc):
    if "start_state" in metafunc.fixturenames:
        metafunc.parametrize("start_state", ["done", "in prog", "todo"])


def test_finish(cards_db, start_state):
    c = Card("write a book", state=start_state)
    index = cards_db.add_card(c)
    cards_db.finish(index)
    card = cards_db.get_card(index)
    assert card.state == "done"

test_finish()函数并没有改变。我们只是改变了pytest在每次测试被调用时填写initial_state值的方式。

我们提供的pytest_generate_tests函数将在pytest建立其要运行的测试列表时被调用。metafunc对象有很多信息,但我们只是用它来获取参数名称和生成参数。

当我们运行它时,这种形式看起来很熟悉。

$ pytest -v test_gen.py
============================= 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
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
collecting ... collected 3 items

test_gen.py::test_finish[done] PASSED                                    [ 33%]
test_gen.py::test_finish[in prog] PASSED                                 [ 66%]
test_gen.py::test_finish[todo] PASSED                                    [100%]

============================== 3 passed in 0.16s ==============================

如果我们想在测试收集时以有趣的方式修改参数化列表,pytest_generate_tests就特别有用。

我们可以将参数化列表建立在一个命令行标志上,因为metafunc让我们可以访问metafunc.config.getoption("--someflag")。也许我们可以添加一个 --excessive 标志来测试更多的值,或者一个 --quick 标志来测试几个值。

参数的参数化列表可以基于另一个参数的存在。例如,对于要求两个相关参数的测试函数,我们可以用一组不同的值对它们进行参数化,而不是在测试时只要求其中一个参数。

例如,我们可以用metafunc.parametrize("planet, moon", [('Earth', 'Moon'), ('Mars', 'Deimos'), ('Mars', 'Phobos'), ...]) 同时对两个相关参数进行参数化。

使用关键词来选择测试用例

参数化技术在快速创建大量测试用例方面相当有效。因此,能够运行测试的一个子集往往是有益的。我们在运行测试子集中第一次看到了-k,但让我们在这里使用它,因为我们在这一章中有相当多的测试案例。

$ pytest -v -k todo
============================= 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
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
collecting ... collected 16 items / 11 deselected / 5 selected

test_finish.py::test_finish_from_todo PASSED                             [ 20%]
test_fix_param.py::test_finish[todo] PASSED                              [ 40%]
test_func_param.py::test_finish[create a course-todo] PASSED             [ 60%]
test_func_param.py::test_finish_simple[todo] PASSED                      [ 80%]
test_gen.py::test_finish[todo] PASSED                                    [100%]

====================== 5 passed, 11 deselected in 0.20s =======================
$ pytest -v -k "todo and not (play or create)"
============================= 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
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
collecting ... collected 16 items / 12 deselected / 4 selected

test_finish.py::test_finish_from_todo PASSED                             [ 25%]
test_fix_param.py::test_finish[todo] PASSED                              [ 50%]
test_func_param.py::test_finish_simple[todo] PASSED                      [ 75%]
test_gen.py::test_finish[todo] PASSED                                    [100%]

====================== 4 passed, 12 deselected in 0.16s =======================
$  pytest -v "test_func_param.py::test_finish"
============================= 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
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
collecting ... collected 3 items

test_func_param.py::test_finish[write a book-done] PASSED                [ 33%]
test_func_param.py::test_finish[second edition-in prog] PASSED           [ 66%]
test_func_param.py::test_finish[create a course-todo] PASSED             [100%]

============================== 3 passed in 0.14s ==============================

$ pytest -v "test_func_param.py::test_finish"
============================= 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
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
collecting ... collected 3 items

test_func_param.py::test_finish[write a book-done] PASSED                [ 33%]
test_func_param.py::test_finish[second edition-in prog] PASSED           [ 66%]
test_func_param.py::test_finish[create a course-todo] PASSED             [100%]

============================== 3 passed in 0.14s ==============================

$ pytest -v "test_func_param.py::test_finish[write a book-done]"
============================= 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
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
collecting ... collected 1 item

test_func_param.py::test_finish[write a book-done] PASSED                [100%]

============================== 1 passed in 0.11s ==============================

你可能感兴趣的:(自动化测试框架pytest教程5-参数化(数据驱动))