自動化測試中如何判斷測試是否通過?詳解Pytest測試框架的斷言用法

軟件測試的主要工作目標是驗證實際結果與預期結果是一致的,在自動化軟件測試中,通過斷言來實現這一目的。Pytest中斷言是通過Python原生的assert語句實現的,對Python原生的assert語句進行了優化,當發生斷言失敗時,錯誤信息更加豐富,方便測試時快速定位問題原因。

正文字數5195

不管是做API測試、Web測試還是APP測試中,測試用例是否執行成功,都是通過比較實際結果與預期結果是否一致來判斷的。當預期結果與實際結果一致,則表示測試用例執行通過,當預期結果與實際結果不一致,則表示測試用例執行失敗。對預期結果與實際結果進行比較的過程,在自動化軟件測試中是通過斷言來實現的。

優秀的測試框架都提供了斷言的方法,比如TestNG中的assertTrue、 assertEquals、assertSame等等。前面給大家介紹過Pytest的使用方法《基於Pytest框架的自動化測試開發實踐(萬字長文入門篇)》,本文將詳細介紹Pytest的斷言,與TestNG相比它更加簡單,只有一個assert語句,但是功能非常強大並且簡單易用。

01 — Python原生的assert

Python中assert語句通常用來對代碼進行必要的檢查,確定某種情況一定發生,或者一定不會發生。

Python 的 assert 語句的語法是這樣的:

assert expression1 ["," expression2]

expression1往往是一個條件表達式,如果條件表達式爲True,則什麼也不做,相當於執行了 pass 語句;如果條件表達式爲False,便會拋出異常 AssertionError,並返回具體的錯誤信息expression2。看一個實際例子:

# content of my_assertion.py
def assertion():
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"if __name__ == '__main__':
    assertion()

執行一下上面的代碼看看結果:

$ python my_assertion.py 
Traceback (most recent call last):
  File "my_assertion.py", line 5, in <module>
    assertion()
  File "my_assertion.py", line 2, in assertion
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
AssertionError: left is [1,2,3], right is [1,2,4]

可見,assert後面的條件表達式爲False,拋出了AssertionError,並顯示了錯誤信息left is [1, 2, 3], right is [1, 2, 4]。

不過,這裏還有一點小小的缺憾。並沒有明確告訴開發人員,條件判斷失敗的具體位置。需要開發人員自己對比才發現,==左邊的第三個元素和右邊的第三個元素不一樣。

02 — Pytest的assert優點

軟件測試工作,經常會遇到斷言失敗的情況。如果每次失敗,都需要測試工程師人眼去觀察失敗的具體原因和出錯的位置,那將是非常耗時的。強大的Pytest也考慮到了廣大測試工程師面臨的問題,因此對Python原生的assert語句進行了優化和改進,主要在是當斷言失敗時,將錯誤的具體信息和位置顯示出來,讓測試工程師對失敗原因一目瞭然。

還是上面的例子,將其放入到測試用例(test_開頭的函數)中:

# content of test_assertion.py
def test_assertion():
    assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
執行測試用例後的信息輸出如下:

    def test_assertion():
>       assert [1, 2, 3] == [1, 2, 4], "left is [1,2,3], right is [1,2,4]"
E       AssertionError: left is [1,2,3], right is [1,2,4]
E       assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4
E         Full diff:
E         - [1, 2, 4]
E         ?        ^
E         + [1, 2, 3]
E         ?

是不是有種很爽的感覺?pytest明確顯示出了錯誤的位置是index爲2的元素左右不相等。這一點點小小的改進大大提高了測試失敗時定位出錯原因的效率。

在測試用例中執行assert語句,纔有上面的效果,這是因爲Pytest對assert語句進行了重寫。在非測試用例中的assert語句,比如測試項目中的一些utils函數中,使用assert還是Python原生的效果。

03 — Pytest 斷言的用法

在自動化測試用例中,最常用的斷言是相等斷言,就是斷言預期結果和實際結果是一致的。通常我們斷言的預期結果和實際結果的數據類型是字符串、元組、字典、列表和對象。Pytest通過assert和==能夠完美支持對這些數據類型的相等斷言。下面來介紹幾種常見的數據類型的斷言操作。

4.1 斷言字符串

斷言字符串非常簡單,只需要將預期和實際的字符串,分別寫在==兩邊,當發生斷言失敗時,將會列出第一個不相等元素的下標。下面是幾個在實際測試工作中經常用到的幾種字符串斷言方式。

# content of test_assertions.py
class TestAssertions(object):
    def test_string_1(self):
        assert "spam" == "eggs"def test_string_2(self):
        assert "foo 1 bar" == "foo 2 bar"def test_string_3(self):
        assert "foo\nspam\nbar" == "foo\neggs\nbar"def test_string_4(self):
        def f():
            return "streaming"
        assert f().startswith('S')

執行一下這些測試用例,看下輸出效果,核心部分如下:

============================================================ FAILURES ============================================================
__________________________________________________ TestAssertions.test_string_1 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911a4d0>
​
    def test_string_1(self):
>       assert "spam" == "eggs"
E       AssertionError: assert 'spam' == 'eggs'
E         - eggs
E         + spam
​
tests/test_assertions.py:3: AssertionError
__________________________________________________ TestAssertions.test_string_2 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911a890>
​
    def test_string_2(self):
>       assert "foo 1 bar" == "foo 2 bar"
E       AssertionError: assert 'foo 1 bar' == 'foo 2 bar'
E         - foo 2 bar
E         ?     ^
E         + foo 1 bar
E         ?     ^
​
tests/test_assertions.py:6: AssertionError
__________________________________________________ TestAssertions.test_string_3 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x10911c2d0>
​
    def test_string_3(self):
>       assert "foo\nspam\nbar" == "foo\neggs\nbar"
E       AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
E           foo
E         - eggs
E         + spam
E           bar
​
tests/test_assertions.py:9: AssertionError
__________________________________________________ TestAssertions.test_string_4 __________________________________________________
​
self = <test_assertions.TestAssertions object at 0x109106a90>
​
    def test_string_4(self):
        def f():
            return "streaming"
    
>       assert f().startswith('S')
E       AssertionError: assert False
E        +  where False = <built-in method startswith of str object at 0x1090f7bb0>('S')
E        +    where <built-in method startswith of str object at 0x1090f7bb0> = 'streaming'.startswith
E        +      where 'streaming' = <function TestAssertions.test_string_4.<locals>.f at 0x10914b440>()
​
tests/test_assertions.py:15: AssertionError

再次感覺到測試結果一目瞭然。

4.2 斷言函數或者接口返回值

對函數返回值、接口返回值的斷言,應該是軟件自動化測試中最常見的場景了。這裏以函數返回值的斷言爲例,

def test_function():
    def f():
        return [1, 2, 3]assert f() == [1, 2, 4]

執行這個測試用例,看下輸出的錯誤信息:

============================================================ FAILURES ============================================================
_________________________________________________________ test_function __________________________________________________________
​
    def test_function():
        def f():
            return [1, 2, 3]
    
>       assert f() == [1, 2, 4]
E       assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4
E         Full diff:
E         - [1, 2, 4]
E         ?        ^
E         + [1, 2, 3]
E         ?        ^
​
tests/test_assertions.py:22: AssertionError
​

可以看到,輸出信息中包含了函數的返回值,並且顯示了返回值與預期結果不一致的元素是index爲2的元素。

4.3 斷言集合類型

斷言列表、元組、字典和集合等類型在測試中也是很常見的,對於具有嵌套的集合數據,pytest的assert依然能夠精確地顯示出來出錯的位置。比如下面這段測試用例代碼:

class TestCollections(object):
    def test_dict(self):
        assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}def test_dict2(self):
        assert {"a": 0, "b": {"c": 0}} == {"a": 0, "b": {"c": 2}}def test_list(self):
        assert [0, 1, 2] == [0, 1, 3]def test_list2(self):
        assert [0, 1, 2] == [0, 1, [1, 2]]def test_set(self):
        assert {0, 10, 11, 12} == {0, 20, 21}

執行上面的測試代碼,核心輸出會是下面這樣:

============================================================ FAILURES ============================================================
___________________________________________________ TestCollections.test_dict ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d2d10>
​
    def test_dict(self):
>       assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
E       AssertionError: assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'b': 1} != {'b': 2}
E         Left contains 1 more item:
E         {'c': 0}
E         Right contains 1 more item:
E         {'d': 0}...
E         
E         ...Full output truncated (6 lines hidden), use '-vv' to show
​
tests/test_assertions.py:27: AssertionError
___________________________________________________ TestCollections.test_dict2 ___________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d2a90>
​
    def test_dict2(self):
>       assert {"a": 0, "b": {"c": 0}} == {"a": 0, "b": {"c": 2}}
E       AssertionError: assert {'a': 0, 'b': {'c': 0}} == {'a': 0, 'b': {'c': 2}}
E         Omitting 1 identical items, use -vv to show
E         Differing items:
E         {'b': {'c': 0}} != {'b': {'c': 2}}
E         Full diff:
E         - {'a': 0, 'b': {'c': 2}}
E         ?                     ^
E         + {'a': 0, 'b': {'c': 0}}...
E         
E         ...Full output truncated (2 lines hidden), use '-vv' to show
​
tests/test_assertions.py:30: AssertionError
___________________________________________________ TestCollections.test_list ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0c1190>
​
    def test_list(self):
>       assert [0, 1, 2] == [0, 1, 3]
E       assert [0, 1, 2] == [0, 1, 3]
E         At index 2 diff: 2 != 3
E         Full diff:
E         - [0, 1, 3]
E         ?        ^
E         + [0, 1, 2]
E         ?        ^
​
tests/test_assertions.py:33: AssertionError
___________________________________________________ TestCollections.test_list2 ___________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0d6c10>
​
    def test_list2(self):
>       assert [0, 1, 2] == [0, 1, [1, 2]]
E       assert [0, 1, 2] == [0, 1, [1, 2]]
E         At index 2 diff: 2 != [1, 2]
E         Full diff:
E         - [0, 1, [1, 2]]
E         ?        ----  -
E         + [0, 1, 2]
​
tests/test_assertions.py:36: AssertionError
____________________________________________________ TestCollections.test_set ____________________________________________________
​
self = <test_assertions.TestCollections object at 0x10b0c1a50>
​
    def test_set(self):
>       assert {0, 10, 11, 12} == {0, 20, 21}
E       AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
E         Extra items in the left set:
E         10
E         11
E         12
E         Extra items in the right set:
E         20
E         21...
E         
E         ...Full output truncated (4 lines hidden), use '-vv' to show
​
tests/test_assertions.py:39: AssertionError

可以看到對於嵌套的字典和列表,也能顯示出不一致數據的具體位置。對於過長的數據,默認是會被truncated,可以通過-vv顯示全部信息。

除了相等斷言,還可以進行大於、小於、不等於、in/not in等類型的斷言。

對於對象的斷言,可以進行對象的類型斷言、對象本身的斷言。這裏就不在一一舉例,只要記住斷言是使用assert語句,使用方法與在Python語言中的使用方法完全一致就可以了。

更多斷言的例子,大家可以參考Pytest的官方文檔:https://docs.pytest.org/en/latest/example/reportingdemo.html,這裏一共有44個斷言的例子,非常全面,幾乎涵蓋了所有的相等斷言的場景。

04 — Pytest斷言Excepiton

除了支持對代碼正常運行的結果斷言之外,Pytest也能夠對Exception和Warnning進行斷言,來斷定某種條件下,一定會出現某種異常或者警告。在功能測試和集成測試中,這兩類斷言用的不多,這裏簡單介紹一下。

對於異常的斷言,Pytest的語法是:with pytest.raises(異常類型),可以看下面的這個例子:

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

這個測試用例斷言運算表達式1除以0會產生ZeroDivisionError異常。除了對異常類型進行斷言,還可以對異常信息進行斷言,比如:

def test_zero_division():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0
    assert 'division by zero' in str(excinfo.value)

這個測試用例,就斷言了excinfo.value的內容中包含division by zero這個字符串,這在需要斷言具體的異常信息時非常有用。

對於Warnning的斷言,其實與Exception的斷言的用法基本一致。這裏就不介紹了,關於更多的Exception和Warnning的斷言可以參考Pytest的官方文檔https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions

05 — 爲斷言添加自定義功能

通過前面的介紹,感覺Pytest的assert挺完美了,又簡單又清晰。但是在實際的測試工作中,還會遇到一些實際問題,比如在斷言時,最好【自動】添加一些日誌,避免我們在測試代碼中手動加入日誌。還有,最好能將斷言的信息,【自動】集成到一些測試報告中,比如Allure中(關於Allure報告大家可以看之前的文章《用Pytest+Allure生成漂亮的HTML圖形化測試報告》)。這樣就能避免在每一個測試腳本中手動寫很多重複的代碼,從而讓我們將更多的時間和精力放到編寫測試用例上。

有了這樣的想法,接下來看看如何實現。

Pytest中提供了一個Hook函數pytest_assertrepr_compare,這個函數會在測試腳本的assert語句執行時被調用。因此,可以實現這個函數,在函數中添加寫日誌和集成allure測試報告代碼。

完整的代碼如下所示:

# content of conftest.py
def pytest_assertrepr_compare(config, op, left, right):
    left_name, right_name = inspect.stack()[7].code_context[0].lstrip().lstrip('assert').rstrip('\n').split(op)
    pytest_output = assertrepr_compare(config, op, left, right)
    logging.debug("{0} is\n {1}".format(left_name, left))
    logging.debug("{0} is\n {1}".format(right_name, right))
    with allure.step("校驗結果"):
        allure.attach(str(left), left_name)
        allure.attach(str(right), right_name)
    return pytest_output

通過inspect獲取調用棧信息,從中得到測試腳本中assert語句中op操作符兩邊的字符串名稱,在日誌和測試報告中會用到。接着執行assertrepr_compare輸出錯誤詳細信息,這些信息就是在執行斷言失敗時的輸出內容,pytest_assertrepr_compare函數沒有對其做任何修改。接着添加了debug日誌輸出和allure測試報告的內容,最後再將assert的錯誤信息返回給調用處。

實現了這個函數後,測試腳本不需要做任何修改,依然是直接使用assert進行斷言。但是能夠自動記錄日誌和生成allure測試報告了。

06 — 禁止Pytest的assert特性

如果不想要Pytest中的assert的效果,而是希望保持Python原生的assert效果,只需要在執行測試是指定一個選項:

--assert=plain

這樣所有測試用例中的assert都變成了Python原生的assert效果了,如果只想某一個模塊保持Python原生的assert效果,那麼就在對應模塊的docstring中添加PYTEST_DONT_REWRITE字符串就好了,也就是在py文件的最上面添加類似下面的docstring內容:

"""
Disable rewriting for a specific module by adding the string:
PYTEST_DONT_REWRITE
"""

不過,我想應該沒有人會這麼幹,因爲Pytest的assert還是更好用一些。

07 — 總結

本文對比了Python原生的assert與Pytest中的assert的區別,詳細介紹了Pytest中assert的用法,並根據測試工作的實際需求,演示瞭如何通過pytest_assertrepr_compare這個Hook函數在斷言時增加日誌和報告輸出。希望對你有幫助。

參考資料

[1] https://morioh.com/tutorials

[2] https://docs.pytest.org/en/latest/example/reportingdemo

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章