pytest之assert斷言實現原理解析


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),  # 除了第一個行,其余都可以縮進
        ]

再次執行:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM