目錄
2.2 如何在測試中編寫和報告斷言
2.2.1 使用assert語句斷言
2.2.2 關于預期異常的斷言
2.2.3 關于預期警告的斷言
2.2.4 應用上下文相關的比較
2.2.5 為失敗的斷言定義自己的解釋
2.2.6 斷言內省細節
2.2 如何在測試中編寫和報告斷言
2.2.1 使用assert語句斷言
pytest允許您使用標準的Python斷言來驗證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-7.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故障報告Demo of Python failure reports with pytest)。這允許您在不丟失內省信息的情況下使用慣用的python構造,而不需要樣板代碼。
但是,如果您指定一條消息,并使用以下斷言:
assert a % 2 == 0, "value was odd, should be even"
那么根本不進行斷言內省,消息將簡單地顯示在回溯中。
有關斷言內省的更多信息,請參閱Assertion introspection details。
2.2.2 關于預期異常的斷言
為了編寫關于引發異常的斷言,可以使用pytest.reses()作為上下文管理器,如下所示:
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。
您可以將match關鍵字參數傳遞給上下文管理器,以測試正則表達式在異常的字符串表示形式上是否匹配(類似于unittest中的TestCase.assertRailesRegex方法):
import pytest def myfunc(): raise ValueError("Exception 123 raised") def test_match(): with pytest.raises(ValueError, match=r".* 123 .*"): myfunc()
match方法的regexp參數與re.search函數匹配,因此在上面的示例中,match='123'也會起作用。
pytest.raises()函數的另一種形式是,您可以傳遞一個將使用給定的*args和**kwargs執行的函數,并斷言引發了給定的異常:
pytest.raises(ExpectedException, func, *args, **kwargs)
在出現諸如無異常或錯誤異常之類的故障時,報告器將為您提供有用的輸出。
請注意,也可以為pytest.mark.xfail指定一個“raises”參數,該參數檢查測試是否以引發任何異常更具體的方式失敗:
@pytest.mark.xfail(raises=IndexError)
def test_f(): f()
使用pytest.reises()可能更適合于測試自己的代碼故意引發的異常的情況,而使用帶有check函數的@pytest.mark.xfail可能更適合記錄未修復的錯誤(測試描述了“應該”發生什么)或依賴關系中的錯誤。
2.2.3 關于預期警告的斷言
您可以使用pytest.warns檢查代碼是否引發特定警告。
2.2.4 應用上下文相關的比較
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-7.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 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 =============================
對一些情況進行了特別比較:
- 比較長字符串:顯示上下文差異
- 比較長序列:首次失敗指數
- 比較dicts:不同條目
有關更多示例,請參閱reporting demo。
2.2.5 為失敗的斷言定義自己的解釋
可以通過實現pytest_asserrepr_compare來添加您自己的詳細解釋。
pytest_asserrepr_compare(config, op, left, right)
返回失敗斷言表達式中的比較說明。
返回None表示沒有自定義解釋,否則返回字符串列表。字符串將由換行符連接,但字符串中的任何換行符都將被轉義。請注意,除第一行外的所有行都將略微縮進,目的是使第一行成為摘要
Parameters
? config (Config) – The pytest config object.
? op (str) – The operator, e.g. "==", "!=", "not in".
? left (object) – The left operand.
? right (object) – The right operand.
例如,考慮在conftest.py文件中添加以下,該文件為Foo對象提供了另一種解釋:
# content of conftest.py
from test_foocompare import Foodef 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.valdef 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
2.2.6 斷言內省細節
關于失敗斷言的報告細節是通過在運行斷言語句之前重寫斷言語句來實現的。重寫斷言語句將自省信息放入斷言失敗消息中。pytest只重寫由其測試收集過程直接發現的測試模塊,因此支持模塊中的斷言(本身不是測試模塊)不會被重寫。
您可以通過在導入模塊之前調用register_assert_rewrite來手動啟用導入模塊的斷言重寫(在根conftest.py中是一個很好的方法)。
為了獲得更多信息,Benjamin Peterson寫了《pytest新斷言重寫的幕后》(Behind the scenes of pytest’s new assertion rewriting)。
斷言重寫在磁盤上緩存文件
pytest會將重寫后的模塊寫回磁盤進行緩存。您可以通過將其添加到conftest.py文件的頂部來禁用此行為(例如,為了避免在大量移動文件的項目中留下過時的.pyc文件):
import sys
sys.dont_write_bytecode = True
請注意,您仍然可以獲得斷言自省的好處,唯一的變化是.pyc文件不會緩存在磁盤上。
此外,如果重寫無法寫入新的.pyc文件,即在只讀文件系統或zipfile中,則重寫將自動跳過緩存。
禁用斷言重寫
pytest在導入時通過使用導入掛鉤寫入新的pyc文件來重寫測試模塊。大多數情況下,這是透明的。但是,如果您自己使用導入機器,則導入鉤子可能會干擾。
如果是這種情況,您有兩種選擇:
- 通過將字符串PYTEST_DONT_REWRITE添加到其文檔字符串中,禁用對特定模塊的重寫。
- 使用--assert=plain禁用所有模塊的重寫。