欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

pyTest官方手册(Release 4.2)之蹩脚翻译(7)

程序员文章站 2022-03-11 16:16:19
...

Chapter 13 参数化

pytest有如下几种参数化的方式:

  • pytest.fixture()可以对测试函数进行参数化
  • @pytest.mark.parametrize允许对测试函数或者测试类定义多个参数和fixtures的集合
  • pytest_generate_tests允许自定义参数化的扩展功能

13.1 @pytest.mark.parametrize: 参数化测试函数

2.2引入,2.4改进
内置的pytest.mark.parametrize装饰器可以用来对测试函数进行参数化处理。下面是一个典型的范例,检查特定的输入所期望的输出是否匹配:

# 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-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
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:8: AssertionError
==================== 1 failed, 2 passed in 0.12 seconds ====================

该示例中,只有一组数据是失败的。通常情况下你可以在traceback中看到作为函数参数的input和output。
注意你也可以对模块或者class使用参数化的marker来让多个测试函数在不同的测试集下运行。
你也可以对参数集中的某个参数使用mark,比如下面使用了内置的mark.xfail:

# test_exception.py
import pytest
@pytest.mark.parametrize("test_input, expected", [("3+5", 8), ("2+4", 6), ("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-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collected 3 items

test_expectation.py ..x                                             [100%]

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

之前结果是失败的用例在这里已经被标记为xfailed了。
如果参数化的列表是一个空列表,比如参数是某个函数动态生成的,请参考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和y的所有组合并进行调用。

13.2 pytest_generate_tests范例

有时你可能需要自定义或者动态的决定参数。你可以使用在collection期间运行的pytest_generate_tests的钩子。该钩子中你可以使用metafunc对象检查请求的上下文,重要的时,你可以调用metafunc.parametrize()来实现参数化。

如下是个从命令行获取参数化的参数的示例:

# test_strings.py
def test_valid_string(stringinput):
    assert stringinput.isalpha()
# 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'))

如果传入两个string,case会运行两次并且pass:

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

传入一个会导致失败的字符:

$ 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 0xdeadbeef>()
E       + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.
˓→isalpha

test_strings.py:3: AssertionError
1 failed in 0.12 seconds

如果不指定,那么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 $REGENDOC_TMPDIR/test_strings.py:1
1 skipped in 0.12 seconds

addoption 增加了一个"–stringinput"选项,格式是追加到一个list中,该list会通过metafunc.config.getoption传递给metafunc.parametrize并生成一个名字为"stringinput"的参数,这个参数(fixture?)会提供给test_valid_string使用。

Chapter 14 缓存:

2.8引入

14.1 用法

插件提供了两种命令行方式来运行上一次pytest测试失败的用例:

  • –lf, --last-failed 仅重新运行失败的用例
  • –ff, --failed-first 先运行上次失败的用例,然后再运行剩余的其他用例

–cache-clear参数用来在开始一轮新的测试前清理所有的缓存。(通常并不需要这么做)
其他插件可以通过访问config.cache对象来设置/获取在pytest调用过程中传递的json格式的值。


注意:这个插件默认是enabled,但是也可以disable掉,参看 Deactivating / unregistering a plugin by name


14.2 重新运行失败的测试或者先运行失败的测试

我们先来创建一个有50次调用的用例,其中有两次测试是失败的:

# test_50.py
import pytest

@pytest.mark.parametrize("i", range(50))
def test_num(i):
    if i in (17, 25):
        pytest.fail("bad luck")

运行这个测试你会看到两个失败的用例:

$ pytest -q
.................F.......F........................ [100%]
================================= FAILURES =================================
_______________________________ test_num[17] _______________________________

i = 17
    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
_______________________________ test_num[25] _______________________________

i = 25

    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
2 failed, 48 passed in 0.12 seconds

如果你使用–lf参数来运行:

$ pytest --lf
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collected 50 items / 48 deselected / 2 selected
run-last-failure: rerun previous 2 failures

test_50.py FF                        [100%]

================================= FAILURES =================================
_______________________________ test_num[17] _______________________________
i = 17
    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            > pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
_______________________________ test_num[25] _______________________________
i = 25
    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            > pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
================= 2 failed, 48 deselected in 0.12 seconds ==================

你会发现只有两个失败的用例被重新运行了一次,其他的48个用例并没有被运行(未被选中)。
现在你再使用–ff选项来运行,所有测试用例都会被运行,而且之前失败的两个用例会首先运行:

$ pytest --ff
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collected 50 items
run-last-failure: rerun previous 2 failures first
test_50.py FF................................................ [100%]
================================= FAILURES =================================
_______________________________ test_num[17] _______________________________
i = 17
    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            > pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
_______________________________ test_num[25] _______________________________
i = 25
    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
            > pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:6: Failed
=================== 2 failed, 48 passed in 0.12 seconds ====================

另外还有类似的参数–nf,–new-first:首先运行新增/修改的用例,然后再运行已有的用例。所有的用例都会按照修改时间来排序。

14.3 前一次没有运行失败的用例时

当上一次测试没有失败的用例,或者没有缓存数据时,pytest可以配置是否运行所有的用例或者不运行任何用例:

pytest --last-failed --last-failed-no-failures all #运行所有用例(默认)
pytest --last-failed --last-failed-no-failures none #不运行任何用例并退出

14.4 config.cache对象

插件或者conftest.py支持代码使用pytest config对象获取一个缓存值。如下是一个简单的例子:

# test_caching.py
import pytest
import time

def expensive_computation():
    print("running expensive computation...")
    
@pytest.fixture
def mydata(request):
    val = request.config.cache.get("example/value", None)
    if val is None:
        expensive_computation()
        val = 42
        request.config.cache.set("example/value", val)
    return val

def test_function(mydata):
    assert mydata == 23

当你第一次运行用例时:

$ pytest -q
F [100%]
================================= FAILURES =================================
______________________________ test_function _______________________________
mydata = 42
    def test_function(mydata):
        > assert mydata == 23
E       assert 42 == 23
E       -42
E       +23

test_caching.py:17: AssertionError
-------------------------- Captured stdout setup ---------------------------
**running expensive computation...**
1 failed in 0.12 seconds

当你再次运行时,你会发现expensive_computation中的打印不会再出现了:

$ pytest -q
F [100%]
================================= FAILURES =================================
______________________________ test_function _______________________________
mydata = 42

    def test_function(mydata):
        > assert mydata == 23
E       assert 42 == 23
E       -42
E       +23

test_caching.py:17: AssertionError
1 failed in 0.12 seconds

14.5 查看缓存内容

你可以通过命令行参数–cache-show来查看缓存内容:

$ pytest --cache-show
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
cachedir: $PYTHON_PREFIX/.pytest_cache
------------------------------- cache values -------------------------------
cache/lastfailed contains:
    {'test_50.py::test_num[17]': True,
    'test_50.py::test_num[25]': True,
    'test_assert1.py::test_function': True,
    'test_assert2.py::test_set_comparison': True,
    'test_caching.py::test_function': True,
    'test_foocompare.py::test_compare': True}
cache/nodeids contains:
    ['test_caching.py::test_function']
cache/stepwise contains:
    []
example/value contains:
42

======================= no tests ran in 0.12 seconds =======================

14.6 清除缓存

你可以使用–cache-clear来清理缓存:
pytest --cache-clear
在那些不希望每次测试之间会可能产生干扰的持续集成服务器上,推荐使用该选项,因为正确性比速度更为重要。

14.7 逐步调试

作为–lf-x的一种替代方法,特别是你预期到你的大部分测试用例都会失败的情况下,使用–sw, --stepwise 允许你每次运行的时候都修复一个用例。测试集将运行到第一个失败的用例,然后自行停止,下一次再调用时,测试集将从上次失败的用例继续运行,然后运行到下一次失败的测试用例再停止。你可以使用–stepwise-skip选项来跳过一个失败的用例,在下一个失败的用例处再停止,如果你一直搞不定当前的失败的用例且只想暂时忽略它,那么这个选项非常有用。

相关标签: pytest