如何使用 skip 和 xfail 处理无法成功的测试

How to use skip and xfail to deal with tests that cannot succeed

你可以标记在某些平台上无法运行的测试函数或预期会失败的测试,以便 pytest 可以相应地处理它们,并在保持测试套件 绿色 的同时呈现测试会话的摘要。

skip 表示你期望你的测试仅在满足某些条件时通过,否则 pytest 应该完全跳过运行该测试。常见的示例是,在非 Windows 平台上跳过仅适用于 Windows 的测试,或者跳过依赖于当前不可用的外部资源(例如数据库)的测试。

xfail 表示你预期某个测试会因某种原因失败。常见示例是测试尚未实现的功能或尚未修复的错误。当一个预期会失败的测试(标记为 pytest.mark.xfail)却意外通过时,它被称为 xpass,并将在测试摘要中报告。

pytest 会单独统计和列出 skipxfail 测试。默认情况下,不会显示跳过/失败测试的详细信息,以避免输出混乱。你可以使用 -r 选项查看与测试进度中显示的“简短”字母相对应的详细信息:

pytest -rxXs  # 显示 xfailed、xpassed 和 skipped 测试的额外信息

有关 -r 选项的更多详细信息,可以通过运行 pytest -h 来找到。

(见 如何更改命令行选项默认值

跳过测试函数

Skipping test functions

跳过测试函数的最简单方法是使用 skip 装饰器标记它,装饰器可以传递一个可选的 reason

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

另外,也可以在测试执行或设置过程中通过调用 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)

Reference: pytest.mark.skip

skipif

``skipif``

如果你希望有条件地跳过某些测试,可以使用 skipif。以下是一个标记测试函数的示例,当在早于 Python 3.10 的解释器上运行时将被跳过:

import sys


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

如果条件在收集期间评估为 True,则测试函数将被跳过,指定的原因将在使用 -rs 时出现在摘要中。

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

# content of test_mymodule.py
import mymodule

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


@minversion
def test_function(): ...

你可以导入标记并在另一个测试模块中重用它:

# test_myothermodule.py
from test_mymodule import minversion


@minversion
def test_anotherfunction(): ...

对于较大的测试套件,通常最好有一个文件来定义标记,然后在整个测试套件中一致地应用这些标记。

另外,你可以使用 condition strings 而不是布尔值,但它们在模块之间不容易共享,因此主要是出于向后兼容性原因而支持。

参考: pytest.mark.skipif

跳过类或模块的所有测试函数

Skip all test functions of a class or module

你可以在类上使用 skipif 标记(与其他标记一样):

@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"

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

如果你想跳过模块的所有测试函数,可以使用 pytestmark 全局变量:

# test_module.py
pytestmark = pytest.mark.skipif(...)

如果多个 skipif 装饰器应用于同一个测试函数,只要任何一个跳过条件为真,该测试函数将被跳过。

跳过文件或目录

Skipping files or directories

有时你可能需要跳过整个文件或目录,例如如果测试依赖于特定于 Python 版本的特性或包含你不希望 pytest 运行的代码。在这种情况下,你必须将文件和目录排除在收集之外。有关更多信息,请参阅 自定义测试收集

跳过缺少的导入依赖项

Skipping on a missing import dependency

你可以通过在模块级别、测试内部或测试设置函数中使用 pytest.importorskip 跳过缺失的导入测试。

docutils = pytest.importorskip("docutils")

如果在此处无法导入 docutils,这将导致测试的跳过结果。你也可以根据库的版本号跳过测试:

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

版本将从指定模块的 __version__ 属性中读取。

摘要

Summary

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

  1. 无条件跳过模块中的所有测试:

pytestmark = pytest.mark.skip("all tests still WIP")
  1. 根据某些条件跳过模块中的所有测试:

pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
  1. 如果缺少某个导入,则跳过模块中的所有测试:

pexpect = pytest.importorskip("pexpect")

XFail:将测试函数标记为预期失败

XFail: mark test functions as expected to fail

你可以使用 xfail 标记来指示你期望一个测试失败:

@pytest.mark.xfail
def test_function(): ...

该测试将会运行,但当它失败时不会报告任何回溯信息。相反,终端报告将其列在“预期失败”(XFAIL)或“意外通过”(XPASS)部分。

另外,你也可以在测试或其设置函数内部以强制方式将测试标记为 XFAIL

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

    if slow_module.slow_function():
        pytest.xfail("slow_module taking too long")

这两个示例说明了在模块级别不想检查条件的情况,此时条件会被评估以标记。

这将使 test_function 成为 XFAIL。请注意,在调用 pytest.xfail() 后不会执行其他代码,这与标记不同。这是因为它通过抛出已知异常在内部实现。

参考: pytest.mark.xfail

condition 参数

condition parameter

如果一个测试只在某个条件下预期失败,你可以将该条件作为第一个参数传递:

@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function(): ...

请注意,你还必须传递一个原因(请参见 pytest.mark.xfail 中的参数说明)。

reason 参数

reason parameter

你可以使用 reason 参数指定预期失败的原因:

@pytest.mark.xfail(reason="known parser issue")
def test_function(): ...

raises 参数

raises parameter

如果你想更具体地说明测试失败的原因,可以在 raises 参数中指定单个异常或异常元组。

@pytest.mark.xfail(raises=RuntimeError)
def test_function(): ...

如果测试失败时引发的异常不在 raises 中,则将其报告为常规失败。

run 参数

run parameter

如果一个测试应该标记为 xfail 并按此报告,但不应该执行,使用 run 参数设置为 False

@pytest.mark.xfail(run=False)
def test_function(): ...

这对于导致解释器崩溃的 xfailing 测试特别有用,这些测试应该在后续进行调查。

strict 参数

strict parameter

默认情况下, XFAILXPASS 不会导致测试套件失败。你可以通过将 strict 关键字参数设置为 True 来更改此行为:

@pytest.mark.xfail(strict=True)
def test_function(): ...

这将使来自该测试的 XPASS (“意外通过”)结果导致测试套件失败。

你可以使用 xfail_strict ini 选项更改 strict 参数的默认值:

[pytest]
xfail_strict=true

忽略 xfail

Ignoring xfail

通过在命令行中指定:

pytest --runxfail

你可以强制运行和报告标记为 xfail 的测试,就好像它根本没有被标记一样。这也会导致 pytest.xfail() 不产生任何效果。

示例

Examples

这是一个包含多种用法的简单测试文件:

from __future__ import annotations

import pytest


xfail = pytest.mark.xfail


@xfail
def test_hello():
    assert 0


@xfail(run=False)
def test_hello2():
    assert 0


@xfail("hasattr(os, 'sep')")
def test_hello3():
    assert 0


@xfail(reason="bug 110")
def test_hello4():
    assert 0


@xfail('pytest.__version__[0] != "17"')
def test_hello5():
    assert 0


def test_hello6():
    pytest.xfail("reason")


@xfail(raises=IndexError)
def test_hello7():
    x = []
    x[1] = 1

使用报告 xfail 选项运行它会产生以下输出:

https://github.com/pytest-dev/pytest/issues/8807

! pytest -rx xfail_demo.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR/example
collected 7 items

xfail_demo.py xxxxxxx                                                [100%]

========================= short test summary info ==========================
XFAIL xfail_demo.py::test_hello
XFAIL xfail_demo.py::test_hello2
reason: [NOTRUN]
XFAIL xfail_demo.py::test_hello3
condition: hasattr(os, 'sep')
XFAIL xfail_demo.py::test_hello4
bug 110
XFAIL xfail_demo.py::test_hello5
condition: pytest.__version__[0] != "17"
XFAIL xfail_demo.py::test_hello6
reason: reason
XFAIL xfail_demo.py::test_hello7
============================ 7 xfailed in 0.12s ============================

使用 parametrize 跳过/xfail

Skip/xfail with parametrize

可以在使用参数化时将标记(如 skip 和 xfail)应用于单个测试实例:

import sys

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