如何在測試中撰寫和回報斷言¶
使用 assert
陳述進行斷言¶
pytest
允許您使用標準 Python assert
來驗證 Python 測試中的預期和值。例如,您可以撰寫以下內容
# content of test_assert1.py
def f():
return 3
def test_function():
assert f() == 4
以斷言您的函式傳回特定值。如果此斷言失敗,您將看到函式呼叫的傳回值
$ pytest test_assert1.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_assert1.py F [100%]
================================= FAILURES =================================
______________________________ test_function _______________________________
def test_function():
> assert f() == 4
E assert 3 == 4
E + where 3 = f()
test_assert1.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_assert1.py::test_function - assert 3 == 4
============================ 1 failed in 0.12s =============================
pytest
支援顯示最常見子表達式的值,包括呼叫、屬性、比較,以及二元和一元運算子。(請參閱 使用 pytest 進行 Python 失敗報告示範)。這允許您使用慣用的 python 建構,而無需樣板程式碼,同時不會遺失內省資訊。
如果使用類似以下方式指定訊息與斷言
assert a % 2 == 0, "value was odd, should be even"
它會與追蹤中的斷言內省一起列印。
請參閱 斷言內省詳細資料 以取得有關斷言內省的更多資訊。
關於預期例外狀況的斷言¶
為了撰寫有關引發例外狀況的斷言,您可以使用 pytest.raises()
作為內容管理員,如下所示
import pytest
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
如果您需要取得實際例外狀況資訊,可以使用
def test_recursion_depth():
with pytest.raises(RuntimeError) as excinfo:
def f():
f()
f()
assert "maximum recursion" in str(excinfo.value)
excinfo
是 ExceptionInfo
執行個體,它是引發實際例外狀況的包裝器。主要的感興趣屬性為 .type
、.value
和 .traceback
。
請注意,pytest.raises
會比對例外類型或任何子類別(例如標準 except
陳述式)。如果您想檢查程式碼區塊是否引發確切的例外類型,您需要明確檢查
def test_foo_not_implemented():
def foo():
raise NotImplementedError
with pytest.raises(RuntimeError) as excinfo:
foo()
assert excinfo.type is RuntimeError
即使函式引發 NotImplementedError
,pytest.raises()
呼叫也會成功,因為 NotImplementedError
是 RuntimeError
的子類別;但是,以下 assert
陳述式會捕捉問題。
比對例外訊息¶
您可以傳遞 match
關鍵字參數給 context-manager,以測試正規表示式是否與例外的字串表示相符(類似於 unittest
中的 TestCase.assertRaisesRegex
方法)
import pytest
def myfunc():
raise ValueError("Exception 123 raised")
def test_match():
with pytest.raises(ValueError, match=r".* 123 .*"):
myfunc()
注意事項
使用
re.search()
函式比對match
參數,因此在上述範例中,match='123'
也能運作。參數
match
也會比對 PEP-678__notes__
。
比對例外群組¶
您也可以使用 excinfo.group_contains()
方法來測試作為 ExceptionGroup
一部分回傳的例外狀況
def test_exception_in_group():
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError("Exception 123 raised"),
],
)
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
assert not excinfo.group_contains(TypeError)
選擇性的 match
關鍵字參數與 pytest.raises()
的運作方式相同。
預設情況下,group_contains()
會遞迴搜尋任何層級的巢狀 ExceptionGroup
實例中的相符例外狀況。如果您只想在特定層級中比對例外狀況,您可以指定 depth
關鍵字參數;直接包含在頂層 ExceptionGroup
中的例外狀況會比對 depth=1
。
def test_exception_in_group_at_given_depth():
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError(),
ExceptionGroup(
"Nested group",
[
TypeError(),
],
),
],
)
assert excinfo.group_contains(RuntimeError, depth=1)
assert excinfo.group_contains(TypeError, depth=2)
assert not excinfo.group_contains(RuntimeError, depth=2)
assert not excinfo.group_contains(TypeError, depth=1)
替代形式 (舊版)¶
有一個替代形式,您可以在其中傳遞一個函式,該函式將會執行,連同 *args
和 **kwargs
,而 pytest.raises()
會使用參數執行函式,並斷言引發了指定的例外狀況
def func(x):
if x <= 0:
raise ValueError("x needs to be larger than zero")
pytest.raises(ValueError, func, x=-1)
如果發生失敗,例如沒有例外狀況或錯誤的例外狀況,報告程式會提供有用的輸出。
此形式是原始的 pytest.raises()
API,在 Python 語言新增 with
陳述式之前開發。如今,此形式很少使用,使用 with
的內容管理員形式被認為更易於閱讀。儘管如此,此形式仍獲得完全支援,且不會以任何方式標示為已棄用。
xfail 標記和 pytest.raises¶
也可以為 pytest.mark.xfail 指定 raises
參數,這會檢查測試失敗的方式是否比僅引發任何例外狀況更具體
def f():
raise IndexError()
@pytest.mark.xfail(raises=IndexError)
def test_f():
f()
如果測試因引發 IndexError
或其子類別而失敗,這才會「xfail」
使用 pytest.mark.xfail 搭配
raises
參數,可能比較適合用來記錄未修正的錯誤(測試描述「應該」發生的情況)或相依項中的錯誤。使用
pytest.raises()
可能比較適合用於測試您自己的程式碼故意引發的例外狀況,這也是大多數的情況。
關於預期警告的斷言¶
您可以使用 pytest.warns 檢查程式碼是否引發特定警告。
使用情境敏感比較¶
pytest
在遇到比較時,會提供豐富的情境敏感資訊。例如
# content of test_assert2.py
def test_set_comparison():
set1 = set("1308")
set2 = set("8035")
assert set1 == set2
如果您執行此模組
$ pytest test_assert2.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_assert2.py F [100%]
================================= FAILURES =================================
___________________________ test_set_comparison ____________________________
def test_set_comparison():
set1 = set("1308")
set2 = set("8035")
> assert set1 == set2
E AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
E
E Extra items in the left set:
E '1'
E Extra items in the right set:
E '5'
E Use -v to get more diff
test_assert2.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
============================ 1 failed in 0.12s =============================
會針對多種情況執行特殊比較
比較長字串:顯示情境差異
比較長序列:第一個失敗的索引
比較字典:不同的項目
請參閱 報告範例 以取得更多範例。
定義您自己的失敗斷言說明¶
您可以透過實作 pytest_assertrepr_compare
掛勾來新增您自己的詳細說明。
- pytest_assertrepr_compare(config, op, left, right)[原始碼]
傳回失敗 assert 表達式中比較項的解釋。
如果沒有自訂解釋,傳回 None,否則傳回字串清單。這些字串會以換行符號串接,但字串內的換行符號會被跳脫。請注意,除了第一行之外,其他行都會稍微縮排,目的是讓第一行成為摘要。
- 參數:
在 conftest 外掛程式中使用¶
任何 conftest 檔案都可以實作這個掛鉤。對於給定的項目,只會諮詢項目目錄及其父目錄中的 conftest 檔案。
以下是一個範例,在 conftest.py 檔案中加入以下掛鉤,為 Foo
物件提供替代解釋
# content of conftest.py
from test_foocompare import Foo
def pytest_assertrepr_compare(op, left, right):
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
return [
"Comparing Foo instances:",
f" vals: {left.val} != {right.val}",
]
現在,給定這個測試模組
# content of test_foocompare.py
class Foo:
def __init__(self, val):
self.val = val
def __eq__(self, other):
return self.val == other.val
def test_compare():
f1 = Foo(1)
f2 = Foo(2)
assert f1 == f2
你可以執行測試模組,並取得 conftest 檔案中定義的自訂輸出
$ pytest -q test_foocompare.py
F [100%]
================================= FAILURES =================================
_______________________________ test_compare _______________________________
def test_compare():
f1 = Foo(1)
f2 = Foo(2)
> assert f1 == f2
E assert Comparing Foo instances:
E vals: 1 != 2
test_foocompare.py:12: AssertionError
========================= short test summary info ==========================
FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
1 failed in 0.12s
斷言內省詳細資料¶
透過在執行斷言陳述前改寫它們,來報告有關失敗斷言的詳細資料。改寫後的斷言陳述會將內省資訊放入斷言失敗訊息中。 pytest
只會改寫其測試收集程序直接發現的測試模組,因此支援模組(本身不是測試模組)中的斷言不會被改寫。
你可以透過在匯入模組前呼叫 register_assert_rewrite 來手動啟用對匯入模組的斷言改寫(一個執行此動作的好地方是根目錄中的 conftest.py
)。
Benjamin Peterson 寫了一篇 揭開 pytest 新斷言改寫的幕後秘辛,提供進一步的資訊。
斷言改寫會快取檔案到磁碟¶
pytest
會將改寫後的模組寫回磁碟以進行快取。你可以停用這個行為(例如,避免在經常移動檔案的專案中留下過期的 .pyc
檔案),方法是在 conftest.py
檔案的頂端加入以下內容
import sys
sys.dont_write_bytecode = True
請注意,您仍可獲得斷言內省的好處,唯一變更為 .pyc
檔案不會快取在磁碟上。
此外,如果無法寫入新的 .pyc
檔案,例如在唯讀檔案系統或 zipfile 中,重寫會靜默略過快取。
停用斷言重寫¶
pytest
透過使用匯入掛鉤來寫入新的 pyc
檔案,在匯入時重寫測試模組。大多數時候,這會透明地運作。但是,如果您自己處理匯入機制,匯入掛鉤可能會造成干擾。
如果是這種情況,您有兩個選項
透過將字串
PYTEST_DONT_REWRITE
加入文件字串,停用特定模組的重寫。透過使用
--assert=plain
停用所有模組的重寫。