使用和调用#
通过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 ================================
这里指定参数
x
、y
的形式是[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
的区域中,报告展示了SKIPPED
、XFAIL
、XPASS
、ERROR
和FAILED
的测试用例,唯独没有展示PASSED
的测试用例。
-r
可以在它的后面紧接着一个或者多个字符,用于指定想要显示的测试用例,默认是fE
,表示总结报告只会显示失败的和发生错误的测试用例。
所有有效的字符参数如下:
f
:失败的;E
:发生错误的;s
:被跳过的;x
:结果为xfailed
的;p
:成功的;P
:成功的,并且有输出信息的。即测试用例中包含print
等行为的;
以及一些特殊的字符参数:
a
:所有的,但是除了pP
的;A
:所有的;N
:因为默认值是fE
,所以我们可以通过它去不打印测试报告;
上述字符参数也可以叠加使用,例如:我们期望过滤出失败的和未执行的:
$ pytest -rfs
失败时加载PDB(Python Debugger)环境#
PDB
是python
内建的诊断器,pytest 允许通过以下命令在执行失败时进入这个诊断器模式:
$ pytest --pdb
pytest 会在每个测试用例失败(或者Ctrl+C
)时,调用这个诊断器。如果你只想在第一次失败时,调用这个诊断器,可以通过以下命令:
# 遇到第一个失败时,调用 PDB 环境,然后退出整个执行过程
$ pytest -x --pdb
# 只有前三个失败用例调用 PDB 环境
$ pytest --pdb --maxfail=3
sys.last_value
、sys.last_type
和sys.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()
被调用,并且PYTHONBREAKPOINT
为None
时,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
属性应该表明测试用例执行的全部耗时,包含setup
和teardown
中的操作,这也是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
这时我们会得到一个告警:
这是因为我们没有在pytest中注册test_id标记,但不影响正常的执行;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] 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
格式的工具有:
- Jenkins 并结合 JUnit 插件;
- Azure Pipelines;
在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
接收两个参数name
和value
以构成<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_property
、record_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插件);