你可以在命令行中通过python解释起调用测试:
python -m pytest [...]
这种方法与在命令行中使用 pytest […] 几乎是等效的,而使用python调用比直接使用pytest多做的事情是程序将把当前目录添加至 sys.path.
运行pytest有六种可能的退出代码:
这些退出代码是在 pytest.ExitCode 中被实现的。它们是公共API,可以在代码中被import和直接使用:
from pytest import ExitCode
提示:如果你想在某些场景下自定义退出代码,特别是“没有测试被收集”这种场景,考虑使用 pytest-custom_exit_code 这个扩展。
pytest --version # 显示pytest是从哪里导入的
pytest --fixtures # 显示可用的内建函数参数
pytest -h | --help # 在命令行上显示帮助和配置文件选项
译者注:pytest --version 经过实际测试,应该是获取当前安装的pytest的版本,这里不是特别清楚是不是官方手册出现了错误
可以在 参考(reference)中查阅到完整的命令行标识。
在失败一次或N次后立即停止测试:
pytest -x # 首次失败立即停止
pytest --maxfail=2 # 两次失败后停止测试
Pytest支持在命令行中以多种方式运行和选择测试用例。
Pytest supports several ways to run and select tests from the command-line.
运行一个模块中的所有测试
pytest test_mod.py
运行一个目录下的所有测试
pytest testing/
通过关键字表达式运行测试
pytest -k "MyClass and not method"
使用上面的方法会运行满足给定的 字符串表达式(忽略大小写)的测试用例,字符表达式可以使用文件名,类名和方法名作为变量,还可以使用python的运算符。例如:上面例子中,TestMyClass.test_something 会运行, TestMyClass.test_method_simple不会运行。
Run tests by node ids
每个收集到的测试都被分配一个唯一的nodeid,该id由模块文件名后面机上说明符(如类名、函数名和参数化的参数)组成,被双冒号分割。
运行一个模块中的特定测试:
pytest test_mod.py::test_func
指定具体测试的另一个例子:
pytest test_mod.py::TestClass::test_method
根据标签表达式(marks)运行:
pytest -m slow # 会运行所有被 @pytest.mark.slow 修饰符修饰的测试,可以在 marks 这一节学到更多有关于标签的知识。
运行包中的测试:
pytest --pyargs pkg.testing
这将导入pkg.testing,并使用它的文件系统位置来查找和运行测试。(译者注:这里的意思就是pytest会导入这个包,并且运行包中的符合我们之前看到的命名规则的pytest的测试)
修改python回显信息的例子:
pytest --showlocals # 在回显中打印本地变量
pytest -l # 在回显中打印本地变量 (简写)
pytest --tb=auto # (默认) 在第一个和最后一个入口使用详尽的回显,其他入口使用简洁回显
pytest --tb=long # 详尽的,信息丰富的回显合适
pytest --tb=short # 简短的信息回显
pytest --tb=line # 每个失败测试只显示一行
pytest --tb=native # Python 标准库的格式
pytest --tb=no # 无回显
使用 --full-trace 可以在发生错误的时候获得非常长的回显(比–tb=long还要长)。这个参数还可以确保当你使用Ctrl+C停止测试运行的时候,一个堆栈跟踪信息会被打印出来。这个技术在测试运行时间过长,而你想让它停下来,看看测试卡在哪里的时候十分有用。默认情况下是不会有任何打印信息的(因为pytest会捕获这个用户操作)。使用这个选项可以保证这个信息被打印出来。
-r 这个选项可以用来在测试报告的末尾显示一个“简短的总结信息”,用以在一个很大的测试集中清楚的了解所有的失败,跳过,xfails,等等。
它默认使用f E来列出失败和错误。
译者注:注意这里的区别,默认情况下也是有简短的总结信息( short test summary info)的,只不过在总结信息中只有 failed 和 error 的用例信息,使用-ra参数,可以让xpassed xfailed skiped 都显示出来(不管是前者还是后者,通过的用例都不会显示)
译者注:xfailed是一种状态,表示本意就是让其失败而结果确实是失败的情况,在后面还会讲解。与之对应的还是xpassed,表示预期失败而实际却成功了的情况。
例子:
# test_example.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
$ pytest -ra
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 6 items
test_example.py .FEsxX [100%]
================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________
@pytest.fixture
def error_fixture():
> assert 0
E assert 0
test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________
def test_fail():
> assert 0
E assert 0
test_example.py:14: AssertionError
========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail
reason: xfailing this test
XPASS test_example.py::test_xpass always xfail
ERROR test_example.py::test_error - assert 0
FAILED test_example.py::test_fail - assert 0
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
-r选项后面接受很多不同的字符,加上‘a’表示“通过的用例之外的其他全部”
以下是可用字符的完整列表:
译者注:读者可以copy上面的例子,尝试-rp 和 -rP的区别,只是输出的两种不同方式,继续往下看,有这部分区别的例子
译者注:-r不能单独使用,必须后接本节列出的这些字符使用
用于组选择的特殊字符:
-r后面接的字符可以组合使用,比如只想看失败的和跳过的用例,你可以执行:
$ pytest -rfs
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 6 items
test_example.py .FEsxX [100%]
================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________
@pytest.fixture
def error_fixture():
> assert 0
E assert 0
test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________
def test_fail():
> assert 0
E assert 0
test_example.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_example.py::test_fail - assert 0
SKIPPED [1] test_example.py:22: skipping this test
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
使用p列出所有通过的测试,而P添加了一个额外的输出块“PASSES”用于显示那些通过了但是有抓取到了标准输出的测试:
$ pytest -rpP
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 6 items
test_example.py .FEsxX [100%]
================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________
@pytest.fixture
def error_fixture():
> assert 0
E assert 0
test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________
def test_fail():
> assert 0
E assert 0
test_example.py:14: AssertionError
================================== PASSES ==================================
_________________________________ test_ok __________________________________
--------------------------- Captured stdout call ---------------------------
ok
========================= short test summary info ==========================
PASSED test_example.py::test_ok
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
译者注:2.8 - 2.11 讲了调试技巧,事实上我们可以使用PyCharm内置的调试器调试程序,浏览这几节,知道有这种方法就OK了。
Python附带一个内置的Python调试器,名字叫做PDB。pytest允许用户使用下面的命令行命令来进行调试:
pytest --pdb
上面的命令会在每次失败的时候(或用户使用键盘Ctrl+C中断)调用Python调试器。通常你可能希望只在第一次错误的时候调用调试器以了解某一个错误的情况:
pytest -x --pdb # 在第一个失败的时候调用PDB,然后结束测试
pytest --pdb --maxfail=3 # 在前三个失败调用PDB
注意:每次失败的异常信息都会被存储在sys.last_value, sys.last_type 和 sys.last_traceback.在python的交互式使用中,这允许使用任何调试工具进行事后调试。用户也可以手动的访问这些异常信息,例子如下:
>>> import sys
>>> sys.last_traceback.tb_lineno
42
>>> sys.last_value
AssertionError('assert result == "ok"',)
pytest允许用户通过命令行的一个选项在开始测试的时候就直接进入PDB调试器:
pytest --trace
这样会在每个测试开始之前调用调试器。
要在代码中设置断点,可以使用Python原生的 import pdb;pdb.set_trace()。pytest会自动的为这个测试禁用输出捕获:
Python 3.7引进了一个内置的 breakpoint() 函数。Pytest支持使用breakpoint() 函数并呈现以下行为:
在版本6.0更改.
获取运行时间比一秒钟长的最慢的十个测试:
pytest --durations=10 --durations-min=1.0
默认情况下,pytest不会显示测试时间太短的测试(<0.005s),除非我们在命令行中使用了-vv选项.
5.0版本新增.
使用错误处理的标准模块可以在程序发生异常或超时的时候转储(dump)python的错误回显信息.
这个模块默认是启用的,除非在命令行中使用 -p no:faulthandler 选项.
faulthandler_timeout=X 这个选项可以配置当一个测试花费的时间超过X秒的时候dump错误信息(在Windows下不可用).
注意:这个功能是从 pytest-faulthandler 插件整合而来,有下面两个区别:
为了创建可以被Jenkins或其他持续集成服务读取的结果文件,使用下面的方法:
pytest --junitxml=path
上面的代码在path上创建了XML文件.
你可以通过在配置文件中设置junit_suite_name来达到设置xml中测试集根节点名称的目的:
[pytest]
junit_suite_name = my_suite
版本4.0新增.
JUnit XML规范似乎表明“time”属性应该报告总的测试执行时间,包括测试准备(setup)和后置处理(teardown)。这是pytest的默认行为。要想只报告调用持续时间,可以像这样配置junit_duration_report选项:
[pytest]
junit_duration_report = call
如果你想为一个测试记录一些额外的信息,你可以使用record_property夹具:
def test_function(record_property):
record_property("example_key", 1)
assert True
这样会在xml的testcase这个节点下,添加一个额外的 example_key=“1” 的属性:
另外,你还可以将这个功能和自定义标签(mark)功能集成:
# 在conftest.py中添加
def pytest_collection_modifyitems(session, config, 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))
在测试代码中添加:
# test_function.py
import pytest
@pytest.mark.test_id(1501)
def test_function():
assert True
会得到下面的信息:
警告:使用这个功能会破坏最新版本的JUnitXML的模式验证.这可能在使用一些CI服务的时候产生问题.
译者注:具体这里是警告什么问题我还没有遇到,以后遇到了再来补充说明,后面再遇到同样的警告就不做过多说明了
给testcase元素添加额外属性,你还可以使用record_xml_attribute夹具.这个方法可以用来覆盖已经存在的属性:
def test_function(record_xml_attribute):
record_xml_attribute("assertions", "REQ-1234")
record_xml_attribute("classname", "custom_classname")
print("hello world")
assert True
相比于record_property,这个夹具不会添加新的子元素,而是会在 testcase 元素中添加assertions="REQ-1234"的内置属性,classname这个属性会被"classname=custom_classname"覆盖:
hello world
警告: record_xml_attribute 是一个实验性的功能,这个接口可能在今后的版本中被更强大或更通用的功能替代.然而,该功能本身会被保留.
当我们在使用ci工具解析报告的时候,使用record_xml_property是非常有用的.有些解析器对于允许的元素和属性是十分严格的.许多工具使用xsd模式验证(比如下面的例子)来验证输入xml.你需要确保使用解析器允许的属性名称.
下面是Jenkins用于验证XML报告的模式验证:
警告:使用这个功能会破坏最新版本的JUnitXML的模式验证.这可能在使用一些CI服务的时候产生问题.
版本4.5新增.
如果你想在test-suite这个级别添加一个与所有测试用例都相关的属性,你可以使用 record_testsuite_property 全局夹具:
record_testsuite_property 可以被用于添加关联到所有测试的属性.
import pytest
@pytest.fixture(scope="session", autouse=True)
def log_global_env_facts(record_testsuite_property):
record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH")
class TestMe:
def test_foo(self):
assert True
这个夹具调用的时候接收一个name,一个value用于在test-suite这个层级添加一个属性,生成的xml如下:
name必须是string类型,value会被转化为string,并且会正确进行xml转义.
生成的XML与最新的xunit标准兼容,这与record_property和record_xml_attribute相反.
要想创建一个纯文本的,供机器阅读的测试报告,你可以使用下面的命令:
pytest --resultlog=path
然后观察path目录下的内容.这些文件会按照 PyPy-test 这个网站显示的例子来显示测试结果.
警告:这个选项已经很少被使用了,计划在6.0版本被移除.如果你要用这个选项,考虑使用新的 pytest-reportlog 扩展.查看 the deprecation docs 来获取更多信息.
“译者注:pastebin.com是一个文本分享网站,可以保存一段文本,生成分享网址等.本节要说的是,pytest可以通过一个命令选项,将执行报告上传,并且提供一个分享的网址,任何拿到分享的人都可以看到测试报告(而这一切都是无需账号的)”
pytest --pastebin=failed
这个命令会提交一个测试运行的信息到远程Paste服务,然后为每一个失败的测试生成一个url.你可以像通常使用时候那样选择测试运行或使用例如 -x 这样的选项选择哪些测试运行结果像被发送到远程服务器.pytest --pastebin=all
目前只实现了粘贴到http://bpaste.net服务。5.2版本修改.
如果由于任何原因创建URL失败,将生成一个警告,而不是整个测试失败。
你可以通过在命令行中明确的使用 -p 选项来预加载插件(内部和外部的):
pytest -p mypluginmodule
这个命令要使用一个名称参数,名称可以是:
pytest -p pytest_cov
在启动的时候禁用某个特定的插件,使用 -p 选项同时使用no:前缀.
例如:要禁用从文本文件执行测试的doctest插件,可以这样调用pytest:
pytest -p no:doctest
你可以在python代码中直接调用pytest:
pytest.main()
这就像从命令行调用“pytest”一样.它不会引发一个 SystemExit(退出代码),而是会返回一个 exitcode(退出代码).你可以给这次调用传递一个选项参数:
pytest.main(["-x", "mytestdir"])
你可以添加特定的插件到 pytest.main 中:
# myinvoke.py
import pytest
class MyPlugin:
def pytest_sessionfinish(self):
print("*** test run reporting finishing")
pytest.main(["-qq"], plugins=[MyPlugin()])
运行它就会显示 MyPlugin 这个插件已经被添加且这个钩子已经被调用了:
$ python myinvoke.py
.FEsxX. [100%]*** test
˓→run reporting finishing
================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________
@pytest.fixture
def error_fixture():
> assert 0
E assert 0
test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________
def test_fail():
> assert 0
E assert 0
test_example.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_example.py::test_fail - assert 0
ERROR test_example.py::test_error - assert 0
译者注:上面的代码实际上就是编写了一个简单的插件,这也给了我们一些启发,在编写一些很多测试都需要用到的公共功能的时候,我们还可以考虑以插件的方式构建我们的测试程序,当然,这也给我们编写插件的能力提出了要求
注意: 调用 pytest.main() 之后会引发你的测试和它import的所有库的导入.基于python import 的高速缓存机制,在同一个进程中在 pytest.main() 调用之后的那些文件(指已经import的文件)变化将不会被发现.由于这个原因,不建议在一个进程中多次调用 pytest.main().(例如:为了在某种情况下重新运行测试)
译者注:这里的提示告诉我们,不要多次调用pytest.main(),也不要想在代码中实现"如果发生…情况,就重新运行所有测试"这种逻辑,测试程序应该有唯一的入口点