使用和调用#

通过python -m pytest的方式执行测试#

我们可以在命令行中通过以下方式执行测试:

python -m pytest [...]

不过这会将当前路径加入到sys.path中,除此之外它与pytest [...]的效果一样。

Note

python -m mod表示将模块(mod)作为脚本来运行,但是模块中必须包含一个__main__.py文件。

pytest 的__main__.py文件内容如下:

"""The pytest entry point."""
import pytest

if __name__ == "__main__":
raise SystemExit(pytest.console_main())

测试结束时可能返回的状态码#

测试执行结束后,pytest 可能会返回以下六种不同的状态码:

  • 0(OK):收集到的所有的测试用例均测试成功;
  • 1(TESTS_FAILED):有用例测试失败;
  • 2(INTERRUPTED):测试执行过程被用户终止,例如:CTRL + C
  • 3(INTERNAL_ERROR):测试执行过程中发生内部错误;
  • 4(USAGE_ERROR):pytest 命令行使用错误;
  • 5(NO_TESTS_COLLECTED):没有收集到任何测试用例;

它们封装在一个枚举类中:pytest.ExitCode,并且可以被直接引入和访问:

from pytest import ExitCode

Note

如果你想在某些场景下自定义返回的状态码,尤其是当未收集到任何测试用例时,可以考虑使用pytest-custom_exit_code插件。

获取帮助信息#

# 显示 pytest 的版本信息
$ pytest --version

# 显示所有可用的 fixture
$ pytest --fixtures

# 显示命令行的帮助信息和配置文件的选项
$ pytest -h | --help

所有可用的命令行选项可以参看:https://docs.pytest.org/en/6.1.1/reference.html#command-line-flags

在第一个(N个)测试用例失败时退出#

# 遇到第一个失败的测试用例时退出
$ pytest -x | --exitfirst

# 遇到第二个失败的测试用例时退出
$ pytest --maxfail=2

执行指定的测试用例#

pytest 支持多种方式从命令行执行指定的测试用例。

执行指定模块中的测试用例#

$ pytest test_mod.py

执行指定目录下所有的测试用例#

$ pytest testing/

执行匹配指定关键字的测试用例#

$ pytest -k "MyClass and not method"

这将执行测试模块名、类名或者函数名匹配指定关键字的测试用例(不区分大小写)。例如,上述例子会执行TestMyClass.test_something,而不会执行TestMyClass.test_method_simple

执行指定nodeid的测试用例#

pytest 为每一个收集到的测试用例指定一个唯一的nodeid,它由模块名说明符构成,以::间隔,其中说明符可以包含类名、函数名和由parametrize标记赋予的参数。

我们来看下面这个例子,这里有三个测试用例,分别对应不同的说明符

# src/chapter-2/test_nodeid.py

import pytest


def test_one():
    pass


class TestNodeid:
    def test_one(self):
        pass

    @pytest.mark.parametrize("x,y", [(1, 2), (3, 4)])
    def test_two(self, x, y):
        assert x + 1 == y
  • 指定函数名

    $ pipenv run pytest src/chapter-2/test_nodeid.py::test_one --collect-only
    ================================ test session starts =================================
    platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
    rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
    collected 1 item
    
    <Module src/chapter-2/test_nodeid.py>
      <Function test_one>
    
    =============================== no tests ran in 0.01s ================================
  • 指定类名+函数名

    $ pipenv run pytest src/chapter-2/test_nodeid.py::TestNodeid::test_one --collect-only
    ================================ test session starts =================================
    platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
    rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
    collected 1 item
    
    <Module src/chapter-2/test_nodeid.py>
      <Class TestNodeid>
          <Function test_one>
    
    =============================== no tests ran in 0.01s ================================
  • 指定parametrize标记赋予的参数

    $ pipenv run pytest src/chapter-2/test_nodeid.py::TestNodeid::test_two[1-2] --collect-only
    ================================ test session starts =================================
    platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
    rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
    collecting ... nodeid src/chapter-2/test_nodeid.py::TestNodeid::test_two[1-2]
    collected 1 item
    
    <Module src/chapter-2/test_nodeid.py>
    <Class TestNodeid>
    <Function test_two[1-2]>
    
    =============================== no tests ran in 0.01s ================================

    这里指定参数xy的形式是[1-1],中间以-间隔,并且只能为[1-1]或者[3-4]

    Attention

    上述方式并未真正执行这些测试用例,只是通过--collect-only展示过滤后收集到的测试用例。

    Note

    我们也可以使用上面介绍的命令行选项-k达到同样的效果,例如:执行test_nodeid.py::test_one测试用例:

    $ pipenv run pytest -k "test_one and not testnodeid" src/chapter-2/test_nodeid.py --collect-only
    ================================ test session starts =================================
    platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
    rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
    collected 4 items / 3 deselected / 1 selected
    
    <Module src/chapter-2/test_nodeid.py>
    <Function test_one>
    
    =============================== 3 deselected in 0.01s ================================

    这和上面的pipenv run pytest src/chapter-2/test_nodeid.py::test_one --collect-only效果是一样的。

执行指定标记的测试用例#

$ pytest -m slow

这会执行所有由@pytest.mark.slow标记的测试用例。

更多详细的信息可以参考:https://docs.pytest.org/en/6.1.1/mark.html#mark

执行指定包中的测试用例#

$ pytest --pyargs pkg.testing

修改回溯信息的输出模式#

# 在回溯信息中显示局部变量
$ pytest -l | --showlocals

# 默认的模式
$ pytest --tb=auto

# 详细的输出
$ pytest --tb=long

# 精简的输出
$ pytest --tb=short

# 每个失败信息总结在一行中
$ pytest --tb=line

# python 的标准模式
$ pytest --tb=native

# 不打印回溯信息
$ pytest --tb=no

使用--full-trace标记会得到更详细的回溯信息(比--tb=long还要详细),并且它甚至会抓取用户强制打断测试执行时的回溯信息(CTRL + C)。这在某些场景非常有用,当你的测试长时间挂死在某一步,你可以使用CTRL + C退出执行来定位问题。

详细的总结报告#

我们可以使用命令行选项-r在执行结束后,打印一个总结报告。

例如,我们有以下测试模块:

# src/chapter-2/test_report.py

import pytest


@pytest.fixture
def error_fixture():
    assert 0


def test_ok():
    print("ok")


def test_fail():
    assert 0


def test_error(error_fixture):
    pass


def test_skip():
    pytest.skip("skipping this test")


def test_xfail():
    pytest.xfail("xfailing this test")


@pytest.mark.xfail(reason="always xfail")
def test_xpass():
    pass

现在,我们想在测试结束后,展示所有没有测试成功的测试用例的报告:

pipenv run pytest -q -ra src/chapter-2/test_report.py
.FEsxX                                                                         [100%]
======================================= ERRORS =======================================
____________________________ ERROR at setup of test_error ____________________________

    @pytest.fixture
    def error_fixture():
>       assert 0
E       assert 0

src/chapter-2/test_report.py:6: AssertionError
====================================== FAILURES ======================================
_____________________________________ test_fail ______________________________________

    def test_fail():
>       assert 0
E       assert 0

src/chapter-2/test_report.py:14: AssertionError
============================== short test summary info ===============================
SKIPPED [1] src/chapter-2/test_report.py:22: skipping this test
XFAIL src/chapter-2/test_report.py::test_xfail
  reason: xfailing this test
XPASS src/chapter-2/test_report.py::test_xpass always xfail
ERROR src/chapter-2/test_report.py::test_error - assert 0
FAILED src/chapter-2/test_report.py::test_fail - assert 0
1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.06s

我们可以看到在short test summary info的区域中,报告展示了SKIPPEDXFAILXPASSERRORFAILED的测试用例,唯独没有展示PASSED的测试用例。

-r可以在它的后面紧接着一个或者多个字符,用于指定想要显示的测试用例,默认是fE,表示总结报告只会显示失败的和发生错误的测试用例。

所有有效的字符参数如下:

  • f:失败的;
  • E:发生错误的;
  • s:被跳过的;
  • x:结果为xfailed的;
  • p:成功的;
  • P:成功的,并且有输出信息的。即测试用例中包含print等行为的;

以及一些特殊的字符参数:

  • a:所有的,但是除了pP的;
  • A:所有的;
  • N:因为默认值是fE,所以我们可以通过它去不打印测试报告;

上述字符参数也可以叠加使用,例如:我们期望过滤出失败的和未执行的:

$ pytest -rfs

失败时加载PDB(Python Debugger)环境#

PDBpython内建的诊断器,pytest 允许通过以下命令在执行失败时进入这个诊断器模式:

$ pytest --pdb

pytest 会在每个测试用例失败(或者Ctrl+C)时,调用这个诊断器。如果你只想在第一次失败时,调用这个诊断器,可以通过以下命令:

# 遇到第一个失败时,调用 PDB 环境,然后退出整个执行过程
$ pytest -x --pdb

# 只有前三个失败用例调用 PDB 环境
$ pytest --pdb --maxfail=3

sys.last_valuesys.last_typesys.last_traceback存储了发生异常时信息,我们可以在 PDB 环境中访问它们。

我们来看下面这个测试用例:

# src/chapter-2/test_pdb.py

def test_pdb():
    x = 0
    assert x

执行测试用例(--pdb):

$ pipenv run pytest src/chapter-2/test_pdb.py --pdb
================================ test session starts =================================
platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
collected 1 item

src/chapter-2/test_pdb.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_pdb():
        x = 0
>       assert x
E       assert 0

src/chapter-2/test_pdb.py:3: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

>>>>>>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>
> /Users/yaomeng/Private/projects/pytest-chinese-doc/src/chapter-2/test_pdb.py(3)test_pdb()
-> assert x
# 需要先引入 sys 模块
(Pdb) import sys
(Pdb) sys.last_value
AssertionError('assert 0')
(Pdb) sys.last_type
<class 'AssertionError'>
# 使用 exit 退出 PDB 环境,使用 continue 继续执行下面的步骤
(Pdb) exit


============================== short test summary info ===============================
FAILED src/chapter-2/test_pdb.py::test_pdb - assert 0
!!!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!!!!
================================= 1 failed in 27.67s =================================

开始执行时就加载PDB环境#

我们可以通过以下命令,在每个测试用例开始时,都先加载PDB环境:

$ pytest --trace

设置断点#

在测试用例代码中添加import pdb;pdb.set_trace(),当其被调用时,pytest会停止这条用例的输出:

  • 其他用例不受影响;
  • 通过continue命令,可以退出PDB环境,并继续执行用例;

使用内置的中断函数#

python 3.7 开始新加了一个内置breakpoint()函数。pytest 支持以下使用行为:

  • breakpoint()被调用,并且PYTHONBREAKPOINTNone时,pytest会使用自定义的PDB代替系统的;
  • 测试执行结束时,自动切回系统自带的PDB
  • 当加上--pdb选项时,breakpoint()和测试发生错误时,都会调用内部自定义的PDB
  • --pdbcls选项允许指定一个用户自定义的PDB类;

Note

我们可以通过--pdbcls=IPython.terminal.debugger:TerminalPdb选项指定ipython为我们的 PDB 调试环境:

$ pipenv run pytest src/chapter-2/test_pdb.py --pdbcls=IPython.terminal.debugger:TerminalPdb --pdb
================================ test session starts =================================
platform darwin -- Python 3.8.4, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /Users/yaomeng/Private/projects/pytest-chinese-doc
collected 1 item

src/chapter-2/test_pdb.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

  def test_pdb():
      x = 0
>       assert x
E       assert 0

src/chapter-2/test_pdb.py:3: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

>>>>>>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>
> /Users/yaomeng/Private/projects/pytest-chinese-doc/src/chapter-2/test_pdb.py(3)test_pdb()
    1 def test_pdb():
    2     x = 0
----> 3     assert x

ipdb> import sys
ipdb> sys.last_type
<class 'AssertionError'>
ipdb> exit


============================== short test summary info ===============================
FAILED src/chapter-2/test_pdb.py::test_pdb - assert 0
!!!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!!!!
================================= 1 failed in 19.02s =================================

分析测试执行时长#

Attention

pytest 6.0 版本开始使用新的行为

列出执行时间超过1秒,最慢的10个测试用例:

$ pytest --durations=10 --durations-min=1.0

默认情况下,pytest 不会列出执行时长小于0.005秒的测试用例,但是我们可以通过添加-vv选项来同时查看它们。

错误句柄#

Attention

pytest 5.0 版本新增特性

在测试执行中发生段错误或者超时的情况下,faulthandler标准模块可以转储python的回溯信息。

它在 pytest 的执行中默认打开,除非使用-p no:faulthandler命令行选项关闭它。

同样,faulthandler_timeout=X配置项,可用于当测试用例的完成时间超过X秒时,转储所有线程的python回溯信息:

# src/chapter-2/pytest.ini

[pytest]
faulthandler_timeout=5

我们有如下测试用例:

# src/chapter-2/test_fault_handler.py 

import time


def test_faulthandler():
    time.sleep(7)

执行测试用例:

$ pipenv run pytest -q src/chapter-2/test_faulthandler.py
Timeout (0:00:05)!
Thread 0x000000010f0505c0 (most recent call first):
  File "/Users/yaomeng/Private/projects/pytest-chinese-doc/src/chapter-2/test_faulthandler.py", line 5 in test_faulthandler
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/python.py", line 184 in pytest_pyfunc_call
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/python.py", line 1627 in runtest
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 163 in pytest_runtest_call
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 256 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 310 in from_call
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 255 in call_runtest_hook
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 216 in call_and_report
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 127 in runtestprotocol
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/runner.py", line 110 in pytest_runtest_protocol
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/main.py", line 338 in pytest_runtestloop
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/main.py", line 313 in _main
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/main.py", line 257 in wrap_session
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/main.py", line 306 in pytest_cmdline_main
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/callers.py", line 187 in _multicall
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 84 in <lambda>
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/manager.py", line 93 in _hookexec
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/pluggy/hooks.py", line 286 in __call__
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/config/__init__.py", line 164 in main
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/config/__init__.py", line 187 in console_main
  File "/Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/bin/pytest", line 8 in <module>
.                                                                              [100%]
1 passed in 6.02s

可以看到,在执行刚超过5秒的时候会打印出回溯信息,但不会中断测试的执行;

关闭faulthandler插件再次执行这个测试用例:

pipenv run pytest -q src/chapter-2/test_faulthandler.py -p no:faulthandler
.                                                                              [100%]
================================== warnings summary ==================================
../../../.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/config/__init__.py:1230
  /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-DA9roatD/lib/python3.8/site-packages/_pytest/config/__init__.py:1230: PytestConfigWarning: Unknown config option: faulthandler_timeout

    self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key))

-- Docs: https://docs.pytest.org/en/stable/warnings.html
1 passed, 1 warning in 6.02s

可以看到,超时并不会触发回溯信息的打印。不过我们会得到一个告警,因为我们关闭了faulthandler插件,那么它定义的faulthandler_timeout配置将无法识别。

Note

这个功能是从pytest-faulthandler插件合并而来的,但是有两点不同:

  • 去使能时,使用-p no:faulthandler代替原来的--no-faulthandler;
  • 使用faulthandler_timeout配置项代替--faulthandler-timeout命令行选项来配置超时时间。当然,你也可以使用-o faulthandler_timeout=X在命令行配置;

创建JUnitXML格式的测试报告#

使用如下命令,可以在指定的path中创建一个能被Jenkins或者其他CI工具读取的XML格式的测试报告:

$ pytest --junitxml=path

你可以在项目的pytest.ini文件中,通过设置junit_suite_name的值,自定义XML文件中testsuite根节点的name信息:

# src/chapter-2/pytest.ini

[pytest]
junit_suite_name = pytest_chinese_doc

Attention

junit_suite_name是 pytest 4.0 版本新增的配置项;

我们先来执行一个测试用例test_nodeid.py::test_one看看效果:

$  pipenv run pytest -q --junitxml=src/chapter-2/report/test_one.xml src/chapter-2/test_nodeid.py::test_one
.                                                                              [100%]
- generated xml file: /Users/yaomeng/Private/projects/pytest-chinese-doc/src/chapter-2/report/test_one.xml -
1 passed in 0.02s

查看生成的XML文件:

<?xml version="1.0" encoding="utf-8"?>

<testsuites>
  <testsuite name="pytest_chinese_doc" errors="0" failures="0" skipped="0" tests="1" time="0.025" timestamp="2020-10-06T10:46:48.256695" hostname="yaomengdeMacBook-Air.local">
    <testcase classname="test_nodeid" name="test_one" time="0.001"/>
  </testsuite>
</testsuites>

我们可以看到,<testsuite>节点的name属性的值,变为我们所期望的pytest_chinese_doc,而不是默认的pytest

JUnit XML规定time属性应该表明测试用例执行的全部耗时,包含setupteardown中的操作,这也是pytest的默认行为;

如果你只想记录测试用例执行的时间,只需要做如下配置:

# src/chapter-2/pytest.ini

[pytest]
junit_duration_report = call

XML报告中为测试用例附加额外的子节点信息#

我们有两种方式实现这个功能:

  • 使用record_property fixture:

    例如:为test_xml_report::test_record_property用例添加一个额外的test_id信息:

    # src/chapter-2/test_xml_report.py
    
    def test_record_property(record_property):
        record_property("test_id", 10010)

    XML文件中的表现为:<property name="test_id" value="10010"/>

    <!-- src/chapter-2/report/test_record_property.xml -->
    
    <?xml version="1.0" encoding="utf-8"?>
    
    <testsuites>
      <testsuite name="pytest_chinese_doc" errors="0" failures="0" skipped="0" tests="1" time="0.021" timestamp="2020-10-06T11:10:59.021407" hostname="yaomengdeMacBook-Air.local">
        <testcase classname="test_xml_report" name="test_record_property" time="0.000">
          <properties>
            <property name="test_id" value="10010"/>
          </properties>
        </testcase>
      </testsuite>
    </testsuites>
  • 新增一个自定义的标记@pytest.mark.test_id():

    首先,在conftest.py文件中重载pytest_collection_modifyitems钩子方法,添加对test_id标记的支持:

    # src/chapter-2/conftest.py
    
    def pytest_collection_modifyitems(items):
        for item in items:
            for marker in item.iter_markers(name="test_id"):
                test_id = marker.args[0]
                item.user_properties.append(("test_id", test_id))

    然后,为测试用例添加新标记:

    # src/chapter-2/test_xml_report.py
    
    import pytest
    
    
    @pytest.mark.test_id(10086)
    def test_marker_test_id():
        pass

    XML文件中的表现也为:<property name="test_id" value="10086"/>

    <?xml version="1.0" encoding="utf-8"?>
    
    <testsuites>
      <testsuite name="pytest_chinese_doc" errors="0" failures="0" skipped="0" tests="2" time="0.025" timestamp="2020-10-06T11:23:30.388331" hostname="yaomengdeMacBook-Air.local">
        <testcase classname="test_xml_report" name="test_marker_test_id" time="0.000">
          <properties>
            <property name="test_id" value="10086"/>
          </properties>
        </testcase>
        <testcase classname="test_xml_report" name="test_record_property" time="0.000">
          <properties>
            <property name="test_id" value="10010"/>
          </properties>
        </testcase>
      </testsuite>
    </testsuites>

    Warning

    这时我们会得到一个告警:

    
    PytestUnknownMarkWarning: Unknown pytest.mark.test_id - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
        @pytest.mark.test_id(10010)
    这是因为我们没有在pytest中注册test_id标记,但不影响正常的执行;

    如果你想去除这个告警,只需要在配置文件中注册这个标记:

    [pytest]
    markers =
        test_id: 为测试用例添加ID

Important

从 pytest 6.0 版本开始,--junit_family命令行选项的默认值改成xunit2,这是对旧的xunit1格式的一种更新,默认情况下,操作此类文件的工具(Jenkins、Azure Pipelines等)均支持该格式。

使用xunit2格式,只需要更新你的配置文件:

[pytest]
junit_family=xunit2

如果你所使用的工具并不支持这种新格式,你也可以继续使用之前的格式:

[pytest]
junit_family=legacy

目前已知的支持xunit2格式的工具有:

XML报告中为测试用例附加额外的属性信息#

可以通过record_xml_attribute fixture 为测试用例附加额外的属性,而不像record_property为其添加子节点;

例如:为测试用例添加一个test_id属性,并修改原先的classname属性:

# src/chapter-2/test_xml_report.py

def test_record_xml_attribute(record_xml_attribute):
    record_xml_attribute("test_id", 10010)
    record_xml_attribute("classname", "custom_classname")

在报告中的表现为<testcase classname="custom_classname" test_id="10010" ...

<!-- src/chapter-2/report/test_record_xml_attribute.xml -->

<?xml version="1.0" encoding="utf-8"?>

<testsuites>
  <testsuite name="pytest_chinese_doc" errors="0" failures="0" skipped="0" tests="1" time="0.024" timestamp="2020-10-06T14:58:12.006400" hostname="yaomengdeMacBook-Air.local">
    <testcase classname="custom_classname" name="test_record_xml_attribute" file="test_xml_report.py" line="12" test_id="10010" time="0.011"/>
  </testsuite>
</testsuites>

Warning

record_xml_attribute目前是一个实验性的功能,未来可能被更强大的 API 所替代,但功能本身会被保留。

XML报告中为测试集附加额外的子节点信息#

Attention

pytest 4.5 版本新增功能

可以通过自定义一个session作用域级别的 fixture,为测试集添加子节点信息,并且会作用于所有的测试用例;

这个自定义的 fixture 需要调用另外一个record_testsuite_property fixture:

record_testsuite_property接收两个参数namevalue以构成<property>标签,其中,name必须为字符串,value会转换为字符串并进行 XML 转义;

# src/chapter-2/test_xml_report.py

@pytest.fixture(scope="session")
def log_global_env_facts(record_testsuite_property):
    record_testsuite_property("EXECUTOR", "luizyao")
    record_testsuite_property("LOCATION", "NJ")


def test_testsuite_property(log_global_env_facts):
    pass

生成的测试报告表现为:在testsuite节点中,多了一个properties子节点,包含所有新增的属性节点,而且,它和所有的testcase节点是平级的;

<!-- src/chapter-2/report/test_testsuite_property.xml -->

<?xml version="1.0" encoding="utf-8"?>

<testsuites>
  <testsuite name="pytest_chinese_doc" errors="0" failures="0" skipped="0" tests="1" time="0.022" timestamp="2020-10-06T15:07:49.768753" hostname="yaomengdeMacBook-Air.local">
    <properties>
      <property name="EXECUTOR" value="luizyao"/>
      <property name="LOCATION" value="NJ"/>
    </properties>
    <testcase classname="test_xml_report" name="test_testsuite_property" file="test_xml_report.py" line="23" time="0.000"/>
  </testsuite>
</testsuites>

这样生成的 XML 文件是符合最新的xunit2标准的,这点和record_propertyrecord_xml_attribute正好相反。如果junit_family=xunit2,它们会触发告警:PytestWarning: record_xml_attribute is incompatible with junit_family 'xunit2' (use 'legacy' or 'xunit1'),而record_testsuite_property不会。

为测试报告提供URL链接 -- pastebin服务#

目前,只实现了在http://bpaste.net上的展示功能;

  • 为每一个失败的测试用例创建一个URL

    pytest --pastebin=failed

    也可以通过添加-x选项,只为第一个失败的测试用例创建一个URL;

  • 为所有的测试用例创建一个URL

    pytest --pastebin=all

尽早的加载插件#

你可以在命令行中使用-p选项,来尽早的加载某一个插件:

pytest -p mypluginmodule

-p选项接收一个name参数,这个参数可以为:

  • 一个完整的本地插件引入,例如:myproject.plugins,其必须是可以import的。

  • 一个公共插件的名称,这是其注册时在setuptools中赋予的名字,例如:尽早的加pytest-cov插件:

    pytest -p pytest_cov

去使能插件#

你可以在命令行中使用-p结合no:,来去使能一个插件的加载,例如:

pytest -p no:doctest

python代码中调用pytest#

可以直接在代码中调用pytest

pytest.main()

这和你在命令行中执行pytest几乎是一样的,但其也有以下特点:

  • 不会触发SystemExit,而是返回exitcode

    # src/chapter-2/test_via_main.py
    
    import time
    
    
    def test_one():
        time.sleep(10)
    
    
    if __name__ == "__main__":
        import pytest
    
        ret = pytest.main(["-q", __file__])
        print(
            "pytest.main() 返回 pytest.ExitCode.INTERRUPTED:",
            ret == pytest.ExitCode.INTERRUPTED,
        )

    用例中有等待10秒的操作,在这期间,打断执行(Ctr+C),pytest.main()返回的是INTERRUPTED状态码;

    pipenv run python src/chapter-2/test_via_main.py
    ^C
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! KeyboardInterrupt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    /Users/yaomeng/Private/projects/pytest-chinese-doc/src/chapter-2/test_via_main.py:5: KeyboardInterrupt
    (to show a full traceback on KeyboardInterrupt use --full-trace)
    no tests ran in 1.38s
    pytest.main() 返回 pytest.ExitCode.INTERRUPTED: True
  • 传递选项和参数:

    pytest.main(["-x", "mytestdir"])
  • 指定一个插件:

    import pytest
    
    
    class MyPlugin:
        def pytest_sessionfinish(self):
            print("*** test run reporting finishing")
    
    
    pytest.main(["-qq"], plugins=[MyPlugin()])

Note

调用pytest.main()会引入你的测试文件以及其引用的所有模块。由于 python 引入机制的缓存特性,当这些文件发生变化时,后续再调用pytest.main()(在同一个程序执行过程中)时,并不会响应这些文件的变化。

基于这个原因,我们不推荐在同一个程序中多次调用pytest.main()(例如:为了重新执行测试;如果你确实有这个需求,或许可以考虑pytest-repeat插件);