3匠题、編寫斷言

1. 使用assert編寫斷言

pytest允許你使用python標(biāo)準(zhǔn)的assert表達(dá)式寫斷言边涕;

例如缔赠,你可以這樣做:

# src/chapter-3/test_sample.py

def func(x):
    return x + 1


def test_sample():
    assert func(3) == 5

如果這個(gè)斷言失敗衍锚,你會(huì)看到func(3)實(shí)際的返回值+ where 4 = func(3)

$ pipenv run pytest -q src/chapter-3/test_sample.py 
F                                                                 [100%]
=============================== FAILURES ================================
______________________________ test_sample ______________________________

    def test_sample():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

src/chapter-3/test_sample.py:28: AssertionError
1 failed in 0.05s

pytest支持顯示常見的python子表達(dá)式的值,包括:調(diào)用嗤堰、屬性戴质、比較混埠、二進(jìn)制和一元運(yùn)算符等(可以參考這個(gè)例子 );

這允許你在沒有模版代碼參考的情況下讨跟,可以使用的python的數(shù)據(jù)結(jié)構(gòu),而無須擔(dān)心自省丟失的問題端衰;

同時(shí)符糊,你也可以為斷言指定了一條說明信息凫海,用于失敗時(shí)的情況說明:

assert a % 2 == 0, "value was odd, should be even"

2. 編寫觸發(fā)期望異常的斷言

你可以使用pytest.raises()作為上下文管理器,來編寫一個(gè)觸發(fā)期望異常的斷言:

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError):
        myfunc()

當(dāng)用例沒有返回ValueError或者沒有異常返回時(shí)男娄,斷言判斷失斝刑啊;

如果你希望同時(shí)訪問異常的屬性模闲,可以這樣:

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError) as excinfo:
        myfunc()
    assert '123' in str(excinfo.value)

其中建瘫,excinfoExceptionInfo的一個(gè)實(shí)例,它封裝了異常的信息尸折;常用的屬性包括:.type啰脚、.value.traceback

注意:

在上下文管理器的作用域中实夹,raises代碼必須是最后一行橄浓,否則,其后面的代碼將不會(huì)執(zhí)行亮航;所以荸实,如果上述例子改成:

def test_match():
  with pytest.raises(ValueError) as excinfo:
      myfunc()
      assert '456' in str(excinfo.value)

則測試將永遠(yuǎn)成功,因?yàn)?code>assert '456' in str(excinfo.value)并不會(huì)執(zhí)行缴淋;

你也可以給pytest.raises()傳遞一個(gè)關(guān)鍵字參數(shù)match准给,來測試異常的字符串表示str(excinfo.value)是否符合給定的正則表達(dá)式(和unittest中的TestCase.assertRaisesRegexp方法類似):

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises((ValueError, RuntimeError), match=r'.* 123 .*'):
        myfunc()

pytest實(shí)際調(diào)用的是re.search()方法來做上述檢查;并且重抖,pytest.raises()也支持檢查多個(gè)期望異常(以元組的形式傳遞參數(shù))露氮,我們只需要觸發(fā)其中任意一個(gè);

pytest.raises還有另外的一種使用形式:

  • 首先钟沛,我們來看一下它在源碼中的定義:

      # _pytest/python_api.py
    
      def raises(  # noqa: F811
          expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
          *args: Any,
          match: Optional[Union[str, "Pattern"]] = None,
          **kwargs: Any
      ) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]:
    

    它接收一個(gè)位置參數(shù)expected_exception畔规,一組可變參數(shù)args,一個(gè)關(guān)鍵字參數(shù)match和一組關(guān)鍵字參數(shù)kwargs讹剔;

  • 接著看方法的具體內(nèi)容:

      # _pytest/python_api.py
    
          if not args:
              if kwargs:
                  msg = "Unexpected keyword arguments passed to pytest.raises: "
                  msg += ", ".join(sorted(kwargs))
                  msg += "\nUse context-manager form instead?"
                  raise TypeError(msg)
              return RaisesContext(expected_exception, message, match)
          else:
              func = args[0]
              if not callable(func):
                  raise TypeError(
                      "{!r} object (type: {}) must be callable".format(func, type(func))
                  )
              try:
                  func(*args[1:], **kwargs)
              except expected_exception as e:
                  # We just caught the exception - there is a traceback.
                  assert e.__traceback__ is not None
                  return _pytest._code.ExceptionInfo.from_exc_info(
                      (type(e), e, e.__traceback__)
                  )
          fail(message)
    

    其中油讯,args如果存在详民,那么它的第一個(gè)參數(shù)必須是一個(gè)可調(diào)用的對(duì)象,否則會(huì)報(bào)TypeError異常陌兑;

    同時(shí)沈跨,它會(huì)把剩余的args參數(shù)和所有kwargs參數(shù)傳遞給這個(gè)可調(diào)用對(duì)象,然后檢查這個(gè)對(duì)象執(zhí)行之后是否觸發(fā)指定異常兔综;

  • 所以我們有了一種新的寫法:

      pytest.raises(ZeroDivisionError, lambda x: 1/x, 0)
    
      # 或者
    
      pytest.raises(ZeroDivisionError, lambda x: 1/x, x=0)
    

    這個(gè)時(shí)候如果你再傳遞match參數(shù)饿凛,是不生效的,因?yàn)樗挥性?code>if not args:的時(shí)候生效软驰;

pytest.mark.xfail()也可以接收一個(gè)raises參數(shù)涧窒,來判斷用例是否因?yàn)橐粋€(gè)具體的異常而導(dǎo)致失敗:

@pytest.mark.xfail(raises=IndexError)
def test_f():
    f()

如果f()觸發(fā)一個(gè)IndexError異常锭亏,則用例標(biāo)記為xfailed纠吴;如果沒有,則正常執(zhí)行f()慧瘤;

注意:

  • 如果f()測試成功戴已,用例的結(jié)果是xpassed,而不是passed锅减;

  • pytest.raises適用于檢查由代碼故意引發(fā)的異常糖儡;而@pytest.mark.xfail()更適合用于記錄一些未修復(fù)的Bug

3. 特殊數(shù)據(jù)結(jié)構(gòu)比較時(shí)的優(yōu)化

# src/chapter-3/test_special_compare.py

def test_set_comparison():
    set1 = set('1308')
    set2 = set('8035')
    assert set1 == set2


def test_long_str_comparison():
    str1 = 'show me codes'
    str2 = 'show me money'
    assert str1 == str2


def test_dict_comparison():
    dict1 = {
        'x': 1,
        'y': 2,
    }
    dict2 = {
        'x': 1,
        'y': 1,
    }
    assert dict1 == dict2

上面怔匣,我們檢查了三種數(shù)據(jù)結(jié)構(gòu)的比較:集合握联、字符串和字典;

$ pipenv run pytest -q src/chapter-3/test_special_compare.py 
FFF                                                               [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         Full diff:
E         - {'8', '0', '1', '3'}
E         + {'8', '3', '5', '0'}

src/chapter-3/test_special_compare.py:26: AssertionError
_______________________ test_long_str_comparison ________________________

    def test_long_str_comparison():
        str1 = 'show me codes'
        str2 = 'show me money'
>       assert str1 == str2
E       AssertionError: assert 'show me codes' == 'show me money'
E         - show me codes
E         ?         ^ ^ ^
E         + show me money
E         ?         ^ ^ ^

src/chapter-3/test_special_compare.py:32: AssertionError
_________________________ test_dict_comparison __________________________

    def test_dict_comparison():
        dict1 = {
            'x': 1,
            'y': 2,
        }
        dict2 = {
            'x': 1,
            'y': 1,
        }
>       assert dict1 == dict2
E       AssertionError: assert {'x': 1, 'y': 2} == {'x': 1, 'y': 1}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'y': 2} != {'y': 1}
E         Full diff:
E         - {'x': 1, 'y': 2}
E         ?               ^
E         + {'x': 1, 'y': 1}...
E         
E         ...Full output truncated (2 lines hidden), use '-vv' to show

src/chapter-3/test_special_compare.py:44: AssertionError
3 failed in 0.08s

針對(duì)一些特殊的數(shù)據(jù)結(jié)構(gòu)間的比較每瞒,pytest對(duì)結(jié)果的顯示做了一些優(yōu)化:

  • 集合金闽、列表等:標(biāo)記出第一個(gè)不同的元素;
  • 字符串:標(biāo)記出不同的部分独泞;
  • 字典:標(biāo)記出不同的條目呐矾;

4. 為失敗斷言添加自定義的說明

# src/chapter-3/test_foo_compare.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

我們定義了一個(gè)Foo對(duì)象,也復(fù)寫了它的__eq__()方法懦砂,但當(dāng)我們執(zhí)行這個(gè)用例時(shí):

$ pipenv run pytest -q src/chapter-3/test_foo_compare.py 
F                                                                 [100%]
=============================== FAILURES ================================
___________________________ test_foo_compare ____________________________

    def test_foo_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert <test_foo_compare.Foo object at 0x10ecae860> == <test_foo_compare.Foo object at 0x10ecae748>

src/chapter-3/test_foo_compare.py:34: AssertionError
1 failed in 0.05s

并不能直觀的從中看出來失敗的原因assert <test_foo_compare.Foo object at 0x10ecae860> == <test_foo_compare.Foo object at 0x10ecae748>

在這種情況下组橄,我們有兩種方法來解決:

  • 復(fù)寫Foo__repr__()方法:

      def __repr__(self):
          return str(self.val)
    

    我們再執(zhí)行用例:

      $ pipenv run pytest -q src/chapter-3/test_foo_compare.py 
      F                                                                 [100%]
      =============================== FAILURES ================================
      ___________________________ test_foo_compare ____________________________
    
          def test_foo_compare():
              f1 = Foo(1)
              f2 = Foo(2)
      >       assert f1 == f2
      E       assert 1 == 2
    
      src/chapter-3/test_foo_compare.py:37: AssertionError
      1 failed in 0.05s
    

    這時(shí)荞膘,我們能看到失敗的原因是因?yàn)?code>1 == 2不成立;

    至于__str__()__repr__()的區(qū)別玉工,可以參考StackFlow上的這個(gè)問題中的回答:https://stackoverflow.com/questions/1436703/difference-between-str-and-repr

  • 使用pytest_assertrepr_compare這個(gè)鉤子方法添加自定義的失敗說明

      # src/chapter-3/test_foo_compare.py
    
      from .test_foo_compare import Foo
    
    
      def pytest_assertrepr_compare(op, left, right):
          if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
              return [
                  "比較兩個(gè)Foo實(shí)例:",  # 頂頭寫概要
                  "   值: {} != {}".format(left.val, right.val),  # 除了第一個(gè)行羽资,其余都可以縮進(jìn)
              ]
    

    再次執(zhí)行:

      $ pytest -q src/chapter-3/test_foo_compare.py 
      F                                                                 [100%]
      =============================== FAILURES ================================
      ___________________________ test_foo_compare ____________________________
    
          def test_foo_compare():
              f1 = Foo(1)
              f2 = Foo(2)
      >       assert f1 == f2
      E       assert 比較兩個(gè)Foo實(shí)例:
      E            值: 1 != 2
    
      src/chapter-3/test_foo_compare.py:37: AssertionError
      1 failed in 0.03s
    

    我們會(huì)看到一個(gè)更友好的失敗說明;

5. 關(guān)于斷言自省的細(xì)節(jié)

當(dāng)斷言失敗時(shí)遵班,pytest為我們提供了非常人性化的失敗說明屠升,中間往往夾雜著相應(yīng)變量的自省信息潮改,這個(gè)我們稱為斷言的自省腹暖;

那么汇在,pytest是如何做到這樣的:

  • pytest發(fā)現(xiàn)測試模塊,并引入他們脏答,與此同時(shí)糕殉,pytest會(huì)復(fù)寫斷言語句,添加自省信息殖告;但是阿蝶,不是測試模塊的斷言語句并不會(huì)被復(fù)寫;

5.1. 復(fù)寫緩存文件

pytest會(huì)把被復(fù)寫的模塊存儲(chǔ)到本地作為緩存使用黄绩,你可以通過在測試用例的根文件夾中的conftest.py里添加如下配置來禁止這種行為羡洁;:

import sys

sys.dont_write_bytecode = True

但是,它并不會(huì)妨礙你享受斷言自省的好處爽丹,只是不會(huì)在本地存儲(chǔ).pyc文件了焚廊。

5.2. 去使能斷言自省

你可以通過一下兩種方法:

  • 在需要去使能模塊的docstring中添加PYTEST_DONT_REWRITE字符串;
  • 執(zhí)行pytest時(shí)习劫,添加--assert=plain選項(xiàng)咆瘟;

我們來看一下去使能后的效果:

$ pipenv run pytest -q --assert=plain src/chapter-3/test_foo_compare.py 
F                                                                 [100%]
=============================== FAILURES ================================
___________________________ test_foo_compare ____________________________

    def test_foo_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       AssertionError

src/chapter-3/test_foo_compare.py:37: AssertionError
1 failed in 0.03s

斷言失敗時(shí)的信息就非常的不完整了,我們幾乎看不出任何有用的調(diào)試信息诽里;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袒餐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子谤狡,更是在濱河造成了極大的恐慌灸眼,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墓懂,死亡現(xiàn)場離奇詭異焰宣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捕仔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門匕积,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榜跌,你說我怎么就攤上這事闪唆。” “怎么了钓葫?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵悄蕾,是天一觀的道長。 經(jīng)常有香客問我础浮,道長帆调,這世上最難降的妖魔是什么奠骄? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮番刊,結(jié)果婚禮上含鳞,老公的妹妹穿的比我還像新娘。我一直安慰自己撵枢,他們只是感情好民晒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锄禽,像睡著了一般潜必。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沃但,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天磁滚,我揣著相機(jī)與錄音,去河邊找鬼宵晚。 笑死垂攘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的淤刃。 我是一名探鬼主播晒他,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逸贾!你這毒婦竟也來了陨仅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤铝侵,失蹤者是張志新(化名)和其女友劉穎灼伤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咪鲜,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狐赡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疟丙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颖侄。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖隆敢,靈堂內(nèi)的尸體忽然破棺而出发皿,到底是詐尸還是另有隱情,我是刑警寧澤拂蝎,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惶室,受9級(jí)特大地震影響温自,放射性物質(zhì)發(fā)生泄漏玄货。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一悼泌、第九天 我趴在偏房一處隱蔽的房頂上張望松捉。 院中可真熱鬧,春花似錦馆里、人聲如沸隘世。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丙者。三九已至,卻和暖如春营密,著一層夾襖步出監(jiān)牢的瞬間械媒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工评汰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纷捞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓被去,卻偏偏與公主長得像主儡,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子惨缆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容