管理 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
的 fixtures 詳細資訊等等。
考慮這個簡單的檔案
# 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
,您甚至會看到以 _
開頭的 fixtures。
支援使用更高的詳細程度級別 (-vvv
、-vvvv
、…),但目前在 pytest 本身中沒有任何效果,但是某些插件可能會利用更高的詳細程度。
細粒度詳細程度¶
除了指定應用程式範圍的詳細程度級別之外,還可以獨立控制特定方面。這是透過在配置檔案中為輸出的特定方面設置詳細程度級別來完成的。
verbosity_assertions
:控制在執行 pytest 時斷言輸出的詳細程度。使用值為 2
執行 pytest --no-header
將具有與先前範例相同的輸出,但檔案中的每個測試都由輸出中的單個字元表示。
verbosity_test_cases
:控制在執行 pytest 時測試執行輸出的詳細程度。使用值為 2
執行 pytest --no-header
將具有與第一個詳細程度範例相同的輸出,但檔案中的每個測試都在輸出中佔用一行。
產生詳細的摘要報告¶
-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 - 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
表示「除了通過之外的所有」。
以下是可以使用的完整可用字元列表
f
- 失敗 (failed)
E
- 錯誤 (error)
s
- 跳過 (skipped)
x
- 預期失敗 (xfailed)
X
- 預期通過 (xpassed)
p
- 通過 (passed)
P
- 通過並帶有輸出 (passed with output)
用於 (取消) 選擇群組的特殊字元
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
分別列印每個跳過的測試。
建立 resultlog 格式檔案¶
若要建立純文字機器可讀取的結果檔案,您可以發出
pytest --resultlog=path
並查看 path
位置的內容。例如,PyPy-test 網頁使用這些檔案來顯示多個修訂版本的測試結果。
建立 JUnitXML 格式檔案¶
若要建立可由 Jenkins 或其他持續整合伺服器讀取的結果檔案,請使用以下調用
pytest --junit-xml=path
在 path
建立 XML 檔案。
若要設定根測試套件 xml 項目的名稱,您可以在配置檔案中配置 junit_suite_name
選項
[pytest]
junit_suite_name = my_suite
在版本 4.0 中新增。
JUnit XML 規範似乎表明 "time"
屬性應報告總測試執行時間,包括設定和拆卸 (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
這會將額外的屬性 example_key="1"
新增至產生的 testcase
標籤
<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¶
若要將額外的 xml 屬性新增至 testcase 元素,您可以使用 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 中新增。
如果您想要在 test-suite 層級新增屬性節點,其中可能包含與所有測試相關的屬性,則可以使用 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 是一個可呼叫物件,它接收 name
和 value
,它們是新增在產生 xml 的 test-suite 層級的 <property>
標籤的名稱和值
<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
標準相容。
將測試報告傳送到線上 pastebin 服務¶
為每個測試失敗建立 URL:
pytest --pastebin=failed
這會將測試運行資訊提交到遠端 Paste 服務,並為每個失敗提供 URL。您可以照常選擇測試,或者新增例如 -x
(如果您只想傳送一個特定的失敗)。
為整個測試會話日誌建立 URL:
pytest --pastebin=all
目前僅實作了貼到 https://bpaste.net/ 服務。
在版本 5.2 中變更。
如果由於任何原因建立 URL 失敗,則會產生警告,而不是讓整個測試套件失敗。