assert斷言實現原理解析
前言
①斷言聲明是用於程序調試的一個便捷方式。
②斷言可以看做是一個 debug 工具,Python 的實現也符合這個設計哲學。
③在 Python 中 assert 語句的執行是依賴於 __debug__ 這個內置變量的,其默認值為True。且當__debug__為True時,assert 語句才會被執行。
擴展: 有時為了調試,我們想在代碼中加一些代碼,通常是一些 print 語句,可以寫為:
# 在代碼中的debug部分,__debug__內置變量的默認值為True【所以正常運行代碼執行if __debug__代碼塊下的調試語句】;當運行程序時加上-o參數,則__debug__內置變量的值為False,不會運行調試語句 if __debug__: pass
一旦調試結束,通過在命令行執行 -O 選項,會忽略這部分代碼: python -o main.py
④若執行python腳本文件時加上 -O 參數,則內置變量 __debug__ 為False。則asser語句不執行。【啟動Python解釋器時可以用 -O 參數來關閉assert語句】
舉例:新建 testAssert.py 腳本文件,內容如下:
print(__debug__) assert 1 > 2
當使用 python testAssert.py 運行時,內置屬性 __debug__ 會輸出 True,assert 1 > 2 語句會拋出 AssertionError 異常。
當使用 python -O testAssert.py 運行時,內置屬性 __debug__ 會輸出 False,assert 1 > 2 語句由於沒有執行不會報任何異常。
assert關鍵字語法
①assert關鍵字語法格式如下:
assert expression
等價於:
if not expression: raise AssertionError
②assert后面也可以緊跟參數:即用戶可以選擇異常的提示值
assert expression [, arguments]
等價於:
if not expression: raise AssertionError(arguments)
③示例如下:
print(__debug__) def foo(s): n = int(s) assert n != 0, 'n is zero!' return 10 / n def main(): foo('0') if __name__ == '__main__': main()
運行結果:

使用assert關鍵字編寫斷言
①pytest允許使用python標准的assert表達式寫斷言;
②pytest支持顯示常見的python子表達式的值,包括:調用、屬性、比較、二進制和一元運算符等(參考:pytest支持的python失敗時報告的演示)
示例:
# test_sample.py def func(x): return x + 1 def test_sample(): assert func(3) == 5
運行結果:

③允許你在沒有模版代碼參考的情況下,可以使用的python的數據結構,而無須擔心丟失自省的問題;
④同時,也可以為斷言指定一條說明信息,用於用例執行失敗時的情況說明:
assert a % 2 == 0, "value was odd, should be even"
觸發期望異常的斷言
①可以使用 with pytest.raises(異常類) 作為上下文管理器,編寫一個觸發期望異常的斷言:
import pytest def test_match(): with pytest.raises(ValueError): raise ValueError("Exception 123 raised")
解釋:當用例沒有返回ValueError或者沒有異常返回時,斷言判斷失敗。
②觸發期望異常的同時訪問異常的屬性
import pytest def test_match(): with pytest.raises(ValueError) as exc_info: raise ValueError("Exception 123 raised") assert '123' in str(exc_info.value)
解釋: exc_info 是 ExceptionInfo 類的一個實例對象,它封裝了異常的信息;常用的屬性包括: type 、 value 和 traceback ;
【注意】在上下文管理器的作用域中,raises代碼必須是最后一行,否則,其后面的代碼將不會執行;所以,如果上述例子改成:
import pytest def test_match(): with pytest.raises(ValueError) as exc_info: raise ValueError("Exception 123 raised") assert '456' in str(exc_info.value)
解釋:拋出異常之后的語句不會執行。
③可以給 pytest.raises() 傳遞一個關鍵字 match ,來測試異常的字符串表示 str(excinfo.value) 是否符合給定的正則表達式(和unittest中的TestCase.assertRaisesRegexp方法類似)
import pytest def test_match(): with pytest.raises((ValueError, RuntimeError), match=r'.* 123 .*'): raise ValueError("Exception 123 raised") # 異常的字符串表示 是否符合 給定的正則表達式
解釋:pytest實際調用的是 re.search() 方法來做上述檢查;並且 ,pytest.raises() 也支持檢查多個期望異常(以元組的形式傳遞參數),我們只需要觸發其中任意一個。
斷言自省
什么是斷言自省?
當斷言失敗時,pytest為我們提供了非常人性化的失敗說明,中間往往夾雜着相應變量的自省信息,這個我們稱為斷言的自省;
那么,pytest是如何做到這樣的:
- pytest發現測試模塊,並引入他們,與此同時,pytest會復寫斷言語句,添加自省信息;但是,不是測試模塊的斷言語句並不會被復寫;
去除斷言自省
兩種方法
- 在需要去除斷言自省模塊的 docstring 的屬性中添加 PYTEST_DONT_REWRITE 字符串;
- pytest命令行方式執行測試用例,添加 --assert=plain 選項;【常用】
示例:
代碼如下:
# test_demo.py class Foo: def __init__(self, val): self.val = val def test_foo_compare(): f1 = Foo(1) f2 = Foo(2) assert f1 == f2
①正常運行結果(未去除斷言自省):

②去除斷言自省:

復寫緩存文件
pytest會把被復寫的模塊存儲到本地作為緩存使用,你可以通過在測試用例的根文件夾中的conftest.py里添加如下配置來禁止這種行為:
import sys sys.dont_write_bytecode = True
但是,它並不會妨礙你享受斷言自省的好處,只是不會在本地存儲 .pyc 文件了。
為失敗斷言添加自定義的說明
代碼如下:
# test_demo.py class Foo: def __init__(self, val): self.val = val def __eq__(self, other): return self.val == other.val def test_foo_compare(): f1 = Foo(1) f2 = Foo(2) assert f1 == f2
運行結果:

此種方式並不能直觀的看出來失敗的原因;
在這種情況下,我們有兩種方法來解決:
①復寫Foo類的 __repr__() 方法:
# test_demo.py class Foo: def __init__(self, val): self.val = val def __eq__(self, other): return self.val == other.val def __repr__(self): return str(self.val) def test_foo_compare(): f1 = Foo(1) f2 = Foo(2) assert f1 == f2
運行結果:
②使用 pytest_assertrepr_compare 鈎子方法添加自定義的失敗說明
# conftest.py from test_demo import Foo def pytest_assertrepr_compare(op, left, right): if isinstance(left, Foo) and isinstance(right, Foo) and op == "==": return [ "比較兩個Foo實例:", # 頂頭寫概要 " 值: {} != {}".format(left.val, right.val), # 除了第一個行,其余都可以縮進 ]
再次執行:

