管理 pytest 的输出¶
修改 Python 回溯打印¶
修改回溯打印的示例
pytest --showlocals # show local variables in tracebacks
pytest -l # show local variables (shortcut)
pytest --no-showlocals # hide local variables (if addopts enables them)
pytest --capture=fd # default, capture at the file descriptor level
pytest --capture=sys # capture at the sys level
pytest --capture=no # don't capture
pytest -s # don't capture (shortcut)
pytest --capture=tee-sys # capture to logs but also output to sys level streams
pytest --tb=auto # (default) 'long' tracebacks for the first and last
# entry, but 'short' style for the other entries
pytest --tb=long # exhaustive, informative traceback formatting
pytest --tb=short # shorter traceback format
pytest --tb=line # only one line per failure
pytest --tb=native # Python standard library formatting
pytest --tb=no # no traceback at all
--full-trace
会在错误时打印非常长的回溯(比 --tb=long
更长)。它还确保在 **KeyboardInterrupt** (Ctrl+C) 时打印堆栈回溯。如果测试运行时间过长,并且您通过 Ctrl+C 中断它们以找出测试“挂起”的位置,这非常有用。默认情况下不会显示任何输出(因为 KeyboardInterrupt 被 pytest 捕获)。使用此选项可确保显示回溯。
详细程度¶
修改打印详细程度的示例
pytest --quiet # quiet - less verbose - mode
pytest -q # quiet - less verbose - mode (shortcut)
pytest -v # increase verbosity, display individual test names
pytest -vv # more verbose, display more details from the test output
pytest -vvv # not a standard , but may be used for even more detail in certain setups
-v
标志控制 pytest 输出的详细程度,包括:测试会话进度、测试失败时的断言细节、--fixtures
时的 fixture 细节等。
考虑这个简单的文件
# content of test_verbosity_example.py
def test_ok():
pass
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
assert fruits1 == fruits2
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
assert number_to_text1 == number_to_text2
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
assert "hello world" in long_text
正常执行 pytest 会得到以下输出(我们跳过头部以专注于其余部分)
$ pytest --no-header
=========================== test session starts ============================
collected 4 items
test_verbosity_example.py .FFF [100%]
================================= FAILURES =================================
_____________________________ test_words_fail ______________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E
E At index 2 diff: 'grapes' != 'orange'
E Use -v to get more diff
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E
E Omitting 1 identical items, use -vv to show
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Use -v to get more diff
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ips... sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================
请注意
文件中的每个测试在输出中显示为一个字符:
.
表示通过,F
表示失败。test_words_fail
失败,我们看到一个简短的摘要,指出两个列表的索引 2 不同。test_numbers_fail
失败,我们看到字典项左右差异的摘要。相同的项被省略。test_long_text_fail
失败,并且in
语句的右侧被截断,使用...`
,因为它比内部阈值(目前 240 个字符)长。
现在我们可以提高 pytest 的详细程度
$ pytest --no-header -v
=========================== test session starts ============================
collecting ... collected 4 items
test_verbosity_example.py::test_ok PASSED [ 25%]
test_verbosity_example.py::test_words_fail FAILED [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED [100%]
================================= FAILURES =================================
_____________________________ test_words_fail ______________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E
E At index 2 diff: 'grapes' != 'orange'
E
E Full diff:
E [
E 'banana',
E 'apple',...
E
E ...Full output truncated (7 lines hidden), use '-vv' to show
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E
E Omitting 1 identical items, use -vv to show
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E ...
E
E ...Full output truncated (16 lines hidden), use '-vv' to show
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================
现在请注意
文件中的每个测试在输出中独占一行。
test_words_fail
现在完整显示两个失败的列表,此外还显示哪个索引不同。test_numbers_fail
现在显示两个字典的文本差异,已截断。test_long_text_fail
不再截断in
语句的右侧,因为内部截断阈值现在更大(目前 2400 个字符)。
现在如果我们进一步增加详细程度
$ pytest --no-header -vv
=========================== test session starts ============================
collecting ... collected 4 items
test_verbosity_example.py::test_ok PASSED [ 25%]
test_verbosity_example.py::test_words_fail FAILED [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED [100%]
================================= FAILURES =================================
_____________________________ test_words_fail ______________________________
def test_words_fail():
fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
> assert fruits1 == fruits2
E AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
E
E At index 2 diff: 'grapes' != 'orange'
E
E Full diff:
E [
E 'banana',
E 'apple',
E - 'orange',
E ? ^ ^^
E + 'grapes',
E ? ^ ^ +
E 'melon',
E 'kiwi',
E ]
test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________
def test_numbers_fail():
number_to_text1 = {str(x): x for x in range(5)}
number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
> assert number_to_text1 == number_to_text2
E AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E
E Common items:
E {'0': 0}
E Left contains 4 more items:
E {'1': 1, '2': 2, '3': 3, '4': 4}
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E
E Full diff:
E {
E '0': 0,
E - '10': 10,
E ? - -
E + '1': 1,
E - '20': 20,
E ? - -
E + '2': 2,
E - '30': 30,
E ? - -
E + '3': 3,
E - '40': 40,
E ? - -
E + '4': 4,
E }
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________
def test_long_text_fail():
long_text = "Lorem ipsum dolor sit amet " * 10
> assert "hello world" in long_text
E AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
At index 2 diff: 'grapes' != 'orange'
Full diff:
[
'banana',
'apple',
- 'orange',
? ^ ^^
+ 'grapes',
? ^ ^ +
'melon',
'kiwi',
]
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
Common items:
{'0': 0}
Left contains 4 more items:
{'1': 1, '2': 2, '3': 3, '4': 4}
Right contains 4 more items:
{'10': 10, '20': 20, '30': 30, '40': 40}
Full diff:
{
'0': 0,
- '10': 10,
? - -
+ '1': 1,
- '20': 20,
? - -
+ '2': 2,
- '30': 30,
? - -
+ '3': 3,
- '40': 40,
? - -
+ '4': 4,
}
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
======================= 3 failed, 1 passed in 0.12s ========================
现在请注意
文件中的每个测试在输出中独占一行。
在这种情况下,
test_words_fail
的输出与之前相同。test_numbers_fail
现在显示两个字典的完整文本差异。test_long_text_fail
也和之前一样不再截断右侧,但现在 pytest 将完全不截断任何文本,无论其大小。
这些是详细程度如何影响正常测试会话输出的示例,但详细程度也用于其他情况,例如如果您使用 pytest --fixtures -v
,您甚至会看到以 _
开头的 fixture。
支持使用更高的详细级别(-vvv
, -vvvv
, ...),但目前在 pytest 本身中没有效果,不过某些插件可能会利用更高的详细级别。
细粒度详细程度¶
除了指定应用程序范围的详细程度级别之外,还可以独立控制特定的方面。这通过在配置文件中为输出的特定方面设置详细程度级别来完成。
verbosity_assertions
:控制执行 pytest 时断言输出的详细程度。pytest --no-header
值为 2
时运行,将与之前的示例具有相同的输出,但文件中的每个测试在输出中显示为一个字符。
verbosity_test_cases
:控制执行 pytest 时测试执行输出的详细程度。pytest --no-header
值为 2
时运行,将与第一个详细程度示例具有相同的输出,但文件中的每个测试在输出中独占一行。
生成详细摘要报告¶
-r
标志可用于在测试会话结束时显示“简短测试摘要信息”,在大规模测试套件中,这使得清晰了解所有失败、跳过、xfail 等变得容易。
它默认为 fE
,以列出失败和错误。
示例
# content of 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-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
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
================================= XPASSES ==================================
========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail - 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
意味着“除了通过的所有”。
以下是可用的完整字符列表
f
- 失败
E
- 错误
s
- 跳过
x
- xfailed(预期失败)
X
- xpassed(预期失败但实际通过)
p
- 通过
P
- 通过并有输出
用于(取消)选择组的特殊字符
a
- 除了pP
之外的所有
A
- 所有
N
- 无,这可用于不显示任何内容(因为fE
是默认值)
可以使用多个字符,例如,要只查看失败和跳过的测试,您可以执行
$ pytest -rfs
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
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-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
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 ===
注意
默认情况下,如果跳过的测试的参数化变体共享相同的跳过原因,它们将被分组在一起。您可以使用 --no-fold-skipped
分别打印每个跳过的测试。
修改截断限制¶
默认的截断限制是 8 行或 640 个字符,以先达到者为准。要设置自定义截断限制,您可以使用以下 pytest.ini
文件选项
[pytest]
truncation_limit_lines = 10
truncation_limit_chars = 90
这将导致 pytest 将断言截断为 10 行或 90 个字符,以先达到者为准。
将 truncation_limit_lines
和 truncation_limit_chars
都设置为 0
将禁用截断。但是,只设置其中一个值将禁用一种截断模式,但保留另一种模式。
创建 JUnitXML 格式文件¶
要创建可由 Jenkins 或其他持续集成服务器读取的结果文件,请使用以下调用
pytest --junit-xml=path
以在 path
处创建 XML 文件。
要设置根测试套件 XML 项的名称,您可以在配置文件中配置 junit_suite_name
选项
[pytest]
junit_suite_name = my_suite
4.0 版本新增。
JUnit XML 规范似乎表明 "time"
属性应报告总测试执行时间,包括 setup 和 teardown(1, 2)。这是 pytest 的默认行为。要仅报告调用持续时间,请像这样配置 junit_duration_report
选项
[pytest]
junit_duration_report = call
record_property¶
如果您想为测试记录额外信息,可以使用 record_property
fixture
def test_function(record_property):
record_property("example_key", 1)
assert True
这将在生成的 testcase
标签中添加一个额外的属性 example_key="1"
<testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
<properties>
<property name="example_key" value="1" />
</properties>
</testcase>
或者,您可以将此功能与自定义标记集成
# content of 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))
在您的测试中
# content of test_function.py
import pytest
@pytest.mark.test_id(1501)
def test_function():
assert True
将产生
<testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
<properties>
<property name="test_id" value="1501" />
</properties>
</testcase>
警告
请注意,使用此功能将破坏最新 JUnitXML 模式的模式验证。当与某些 CI 服务器一起使用时,这可能会成为问题。
record_xml_attribute¶
要向 testcase 元素添加额外的 XML 属性,您可以使用 record_xml_attribute
fixture。这也可以用于覆盖现有值
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=custom_classname"
覆盖默认的 classname
<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
<system-out>
hello world
</system-out>
</testcase>
警告
record_xml_attribute
是一个实验性功能,其接口在未来版本中可能会被更强大、更通用的东西取代。但其功能本身将保留。
当使用 CI 工具解析 XML 报告时,使用此功能而不是 record_xml_property
可能会有所帮助。然而,一些解析器对允许的元素和属性非常严格。许多工具使用 XSD 模式(如下面的示例)来验证传入的 XML。请确保您使用的属性名称被您的解析器允许。
以下是 Jenkins 用于验证 XML 报告的模式
<xs:element name="testcase">
<xs:complexType>
<xs:sequence>
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="assertions" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="classname" type="xs:string" use="optional"/>
<xs:attribute name="status" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
警告
请注意,使用此功能将破坏最新 JUnitXML 模式的模式验证。当与某些 CI 服务器一起使用时,这可能会成为问题。
record_testsuite_property¶
4.5 版本新增。
如果您想在测试套件级别添加一个 properties 节点,其中可能包含与所有测试相关的属性,您可以使用 record_testsuite_property
会话级 fixture
record_testsuite_property
会话级 fixture 可用于添加与所有测试相关的属性。
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
该 fixture 是一个可调用对象,它接收在生成的 XML 的测试套件级别添加的 <property>
标签的 name
和 value
。
<testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
<properties>
<property name="ARCH" value="PPC"/>
<property name="STORAGE_TYPE" value="CEPH"/>
</properties>
<testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
</testsuite>
name
必须是字符串,value
将被转换为字符串并正确进行 XML 转义。
与 record_property 和 record_xml_attribute 相反,生成的 XML 与最新的 xunit
标准兼容。
将测试报告发送到在线粘贴服务¶
为每个测试失败创建 URL:
pytest --pastebin=failed
这将把测试运行信息提交到远程 Paste 服务,并为每个失败提供一个 URL。您可以像往常一样选择测试,或者例如添加 -x
如果您只想发送一个特定的失败。
为整个测试会话日志创建 URL:
pytest --pastebin=all
目前只实现了粘贴到 https://bpaste.net/ 服务。
5.2 版本更改。
如果创建 URL 因任何原因失败,将生成警告而不是使整个测试套件失败。