pytest skip、xfail及参数化

一、pytest skip和xfail标记

实际工作中,测试用例的执行可能会依赖于一些外部条件,例如:只能运行在某个特定的操作系统(Windows),或者我们本身期望它们测试失败,例如:被某个已知的Bug所阻塞;如果我们能为这些用例提前打上标记,那么pytest就相应地预处理它们,并提供一个更加准确的测试报告;

在这种场景下,常用的标记有:

  • skip:只有当某些条件得到满足时,才执行测试用例,否则跳过整个测试用例的执行;例如,在非Windows平台上跳过只支持Windows系统的用例;
  • xfail:因为一个确切的原因,我们知道这个用例会失败;例如,对某个未实现的功能的测试,或者阻塞于某个已知Bug的测试;

pytest默认不显示skipxfail用例的详细信息,我们可以通过-r选项来自定义这种行为;

例如,显示结果为XFAILXPASSSKIPPED的用例:

pytest -rxXs  # show extra info on xfailed, xpassed, and skipped tests

1、跳过测试用例的执行

  • @pytest.mark.skip装饰器

跳过测试用例的执行最简单方法是用skip装饰器对其进行标记,该装饰器可以通过可选的传递reason

@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
    ...
  • pytest.skip方法

另外,也可以通过调用以下pytest.skip(reason)函数在测试执行或设置期间强制跳过:

def test_function():
    if not valid_config():
        pytest.skip("unsupported configuration")

也可以在模块级别跳过整个模块 :pytest.skip(reason, allow_module_level=True)

import sys
import pytest

if not sys.platform.startswith("win"):
    pytest.skip("skipping windows-only tests", allow_module_level=True)

注意:

当在用例中设置allow_module_level参数时,并不会生效;

def test_one():
    pytest.skip("跳出", allow_module_level=True)

def test_two():
    assert 1

也就是说,在上述示例中,并不会跳过test_two用例;

  • @pytest.mark.skipif装饰器

如果我们想有条件的跳过某些测试用例的执行,可以使用@pytest.mark.skipif装饰器;

例如,当python的版本小于3.6时,跳过用例:

import sys

@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
def test_function():
    ...

可以skipif在模块之间共享标记。考虑以下测试模块:

import mymodule

minversion = pytest.mark.skipif(
    mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
)

@minversion
def test_function():
    ...

可以导入标记并将其在另一个测试模块中重复使用:

from test_mymodule import minversion

@minversion
def test_anotherfunction():
    ...

可以看到,minversion在两个测试模块中都生效了;

因此,在大型的测试项目中,可以在一个文件中定义所有的执行条件,需要时再引入到模块中;

另外,需要注意的是,当一个用例指定了多个skipif条件时,只需满足其中一个,就可以跳过这个用例的执行;

2、跳过类的所有测试功能

如果条件为True,则该标记将为该类的每种测试方法产生跳过结果。

@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestPosixCalls:
    def test_function(self):
        "will not be setup or run under 'win32' platform"

3、跳过模块的所有测试功能

在模块中定义pytestmark变量(推荐):

# src/chapter-10/test_skip_module.py
import pytest

pytestmark = pytest.mark.skip('作用于模块中的每一个用例,所以 pytest 共收集到两个 SKIPPED 的用例。')

def test_one():
    assert True

def test_two():
    assert True

或者,在模块中调用pytest.skip方法,并设置allow_module_level=True

# src/chapter-10/test_skip_module.py

import pytest

pytest.skip('在用例收集阶段就已经跳出了,所以不会收集到任何用例。', allow_module_level=True)

def test_one():
    assert True

def test_two():
    assert True

4、跳过指定文件或目录

通过在conftest.py中配置collect_ignore_glob项,可以在用例的收集阶段跳过指定的文件和目录;

例如,跳过当前测试目录中文件名匹配test_*.py规则的文件和config的子文件夹sub中的文件:

collect_ignore_glob = ['test*.py', 'config/sub']

5、跳过导入模块失败依赖项

当引入某个模块失败时,我们同样可以跳过后续部分的执行;

docutils = pytest.importorskip("docutils")

我们也可以为其指定一个最低满足要求的版本,判断的依据是检查引入模块的__version__ 属性:

docutils = pytest.importorskip("docutils", minversion="0.3") 

我们注意到pytest.importorskippytest.skip(allow_module_level=True)都可以在模块的引入阶段跳过剩余部分;实际上,在源码中它们抛出的都是同样的异常:

# pytest.skip(allow_module_level=True)

raise Skipped(msg=msg, allow_module_level=allow_module_level)
# pytest.importorskip()

raise Skipped(reason, allow_module_level=True) from None

只是importorskip额外增加了minversion参数:

# _pytest/outcomes.py
 
if minversion is None:
        return mod
    verattr = getattr(mod, "__version__", None)
    if minversion is not None:
        if verattr is None or Version(verattr) < Version(minversion):
            raise Skipped(
                "module %r has __version__ %r, required is: %r"
                % (modname, verattr, minversion),
                allow_module_level=True,
            )

从中我们也证实了,它实际检查的是模块的__version__属性;

所以,对于一般场景下,使用下面的方法可以实现同样的效果:

try:
    import docutils
except ImportError:
    pytest.skip("could not import 'docutils': No module named 'docutils'",
                allow_module_level=True)

6、skip小结

这是有关如何在不同情况下跳过模块中的测试的快速指南:

  • 无条件跳过模块中的所有测试:
pytestmark = pytest.mark.skip("all tests still WIP")
  • 根据某些条件跳过模块中的所有测试:
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
  • 如果缺少某些导入,请跳过模块中的所有测试:
pexpect = pytest.importorskip("pexpect")

7、@pytest.mark.xfail的用法

我们可以使用@pytest.mark.xfail标记用例,表示期望这个用例执行失败;

用例会正常执行,只是失败时不再显示堆栈信息,最终的结果有两个:用例执行失败时(XFAIL:符合预期的失败)、用例执行成功时(XPASS:不符合预期的成功)

另外,我们也可以通过pytest.xfail方法在用例执行过程中直接标记用例结果为XFAIL,并跳过剩余的部分:

def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")

同样可以为pytest.xfail指定一个reason参数,表明原因;

下面我们来重点看一下@pytest.mark.xfail的用法:

  • condition位置参数,默认值为None

    @pytest.mark.skipif一样,它也可以接收一个python表达式,表明只有满足条件时才标记用例;

    例如,只在pytest 3.6版本以上标记用例:

    @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes")
    def test_function():
        ...
    
  • reason关键字参数,默认值为None

    可以指定一个字符串,表明标记用例的原因;

  • strict关键字参数,默认值为False

    strict=False时,如果用例执行失败,结果标记为XFAIL,表示符合预期的失败;如果用例执行成功,结果标记为XPASS,表示不符合预期的成功;

    strict=True时,如果用例执行成功,结果将标记为FAILED,而不再是XPASS了;

    我们也可以在pytest.ini文件中配置:

    [pytest]
    xfail_strict=true
    
  • raises关键字参数,默认值为None

    可以指定为一个异常类或者多个异常类的元组,表明我们期望用例上报指定的异常;

    如果用例的失败不是因为所期望的异常导致的,pytest将会把测试结果标记为FAILED;

  • run关键字参数,默认值为True:

    run=False时,pytest不会再执行测试用例,直接将结果标记为XFAIL

8、忽略xfail标记

通过在命令行上指定:

pytest --runxfail

您可以强制运行和报告已xfail标记的测试,就像根本没有标记一样。这也pytest.xfail不会产生任何效果。

9、skip和xfai参数化

pytest.param方法可用于为@pytest.mark.parametrize或者参数化的fixture指定一个具体的实参,它有一个关键字参数marks,可以接收一个或一组标记,用于标记这轮测试的用例;

我们以下面的例子来说明:

import pytest

@pytest.mark.parametrize(
    ("n", "expected"),
    [
        (1, 2),
        pytest.param(1, 0, marks=pytest.mark.xfail),
        pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
        (2, 3),
        (3, 4),
        (4, 5),
        pytest.param(
            10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k")
        ),
    ],
)
def test_increment(n, expected):
    assert n + 1 == expected

二、pytest 参数化

1、@pytest.mark.parametrize装饰器

内置的pytest.mark.parametrize装饰器为测试函数启用参数的参数化。这是一个测试功能的典型示例,该功能实现检查某些输入是否导致期望的输出:

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-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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
======================= 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

但是请记住,根据所使用的操作系统和当前安装的插件,这可能会导致不良的副作用,甚至导致错误,因此使用此方法后果自负。

请注意,您还可以在类或模块上使用参数标记(请参阅使用attribute标记测试功能),这将使用参数集调用多个函数。

2、参数化中标记各个测试实例

也可以在参数化中标记各个测试实例,例如使用内置的mark.xfail:

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

执行:

======================== 2 passed, 1 xfailed in 0.10s =========================

Process finished with exit code 0
..x
test_input = '6*9', expected = 42

    @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
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

先前导致失败的一个参数集现在显示为“ xfailed(预期失败)”测试。

3、多个参数化参数

要获得多个参数化参数的所有组合,可以堆叠 parametrize装饰器:

import pytest

@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass

参考更多参数化例子:https://docs.pytest.org/en/latest/example/parametrize.html#paramexamples

posted @ 2020-03-04 18:09  xyztank  阅读(588)  评论(0)    收藏  举报