Pytest 參數化
pytest 中的 參數化 功能一開始可能看起來很複雜,但其用途很簡單,一旦您瞭解它解決的問題。 基本上, 參數化 可讓您以不同的輸入有效率地執行相同的測試函式,讓您更輕鬆地以較少的程式碼執行詳細且不同的判斷提示。
呼叫 參數化時,第一個自變數是包含一或多個自變數名稱的字串,例如 "test\_input_"。 第二個自變數包含自變數值的清單,例如 ["27", "6+9", "0", "O"]。 最後四個自變數具有預設值,而且是選擇性的。
您可以在這裡找到英文 pytest API 參考 parametrize:pytest.Metafunc.parametrize。
使用參數化的時機
您可能想要使用 參數化 的兩個常見情境包括:
- 當測試for迴圈時
- 當多個測試斷言相同行為時
讓我們先檢閱每個範例,而不使用 參數化,然後使用它來示範如何改善我們的測試。
For 迴圈
以下是具有 for 循環的測試函式範例:
def test_string_is_digit():
items = ["1", "10", "33"]
for item in items:
assert item.isdigit()
此測試有問題,因為如果失敗,可能會導致數個問題,包括:
- 模棱兩可的測試報告: 測試報告不會釐清是否只有一個項目失敗,或是否有多個失敗。
- 單一測試檢視: 所有專案都會被視為單一測試,這會遮蔽個別專案效能。
- 不確定的修正: 如果修正失敗,則無法知道所有問題是否已解決,而不需要重新執行整個測試。
讓我們修改測試,以特別包含預期失敗的兩個項目:
def test_string_is_digit():
items = ["No", "1", "10", "33", "Yes"]
for item in items:
assert item.isdigit()
執行測試時,即使該清單中有兩個無效的項目,結果也只會顯示一個失敗項目。
$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x103fa1df0>()
E + where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================
這是 參數化的絕佳使用案例。 在查看如何更新測試之前,讓我們先探索另一個不涉及 for 迴圈的常見情況。
斷言相同行為的測試
執行相同判斷提示的測試群組也非常適合使用參數化。 如果先前的測試被重寫為對每個項目進行一次測試,那麼這將允許更好的失敗報告,但會重複:
def test_is_digit_1():
assert "1".isdigit()
def test_is_digit_10():
assert "10".isdigit()
def test_is_digit_33():
assert "33".isdigit()
這些測試較好,在於失敗很容易與單一輸入產生關聯。 雖然有一些類似的測試似乎不尋常,但在嘗試更細微的生產測試套件中很常見。
雖然測試會更好,因為它們可以確切報告失敗(或通過)的內容,但也會出現下列問題:
- 程式代碼是重複的,這會造成維護負擔
- 更新測試時可能會發生錯字和錯誤
- 因為它們是重複的,工程師可能不會包含所有使用案例和輸入
如何使用參數設定
既然您已了解參數化的一些使用案例,那我們就來更新使用包含失敗項目的 for 迴圈的測試。
若要使用 參數化,您必須將 pytest 引入為程式庫,然後將其作為函式中的裝飾器使用。 以下是更新的測試:
import pytest
@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
assert item.isdigit()
在執行測試之前,我們來回顧一下變更。
修飾器pytest.mark.parametrize()定義兩個參數。 第一個自變數是稱為 "item"的字串。 該字串將作為測試函式定義中下一行您看到的測試函式的具名參數使用。 第二個自變數是測試值的清單。
豐富的錯誤報告
在幕後,pytest 會將該清單中的每個項目 視為個別的測試。 這表示通過和失敗的測試會分別報告。 讓我們看看當執行pytest測試時會發生什麼事:
$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py F...F [100%]
=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x102d45e30>()
E + where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x102d45df0>()
E + where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================
測試報告中有幾個值得注意的項目。 首先,我們看到在單一測試中,pytest 總共報告了五個測試:三個通過,兩個未通過。 系統會個別回報失敗情況,包括失敗的輸入為何。
$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E + where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
回報的位置這麼多,因此您不太可能錯過造成失敗的值。
使用詳細資訊輸出旗標
在命令列中執行測試時,系統會盡量減少測試通過時的回報。 以下是測試在更新之後的外觀,以修正失敗:
@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
assert item.isdigit()
執行測試會產生最少的輸出:
$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py ..... [100%]
============================== 5 passed in 0.01s ===============================
提升詳細程度可以顯示 Pytest 在使用參數化時為每個測試執行的值:
$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py::test_string_is_digit[0] PASSED [ 20%]
test_items.py::test_string_is_digit[1] PASSED [ 40%]
test_items.py::test_string_is_digit[10] PASSED [ 60%]
test_items.py::test_string_is_digit[33] PASSED [ 80%]
test_items.py::test_string_is_digit[9] PASSED [100%]
============================== 5 passed in 0.01s ===============================
如何使用多個自變數名稱
到目前為止,我們看到的範例在第一個自變數中只有一個自變數名稱。 我們已使用 "item" ,但您可以在字串中包含多個自變數名稱,以指定以逗號分隔的第一個自變數。
使用多個自變數名稱的其中一個使用案例是,如果您想要傳入一組預期的值,以針對您的輸入值進行測試。 在第二個自變數中,集合中的每個項目都必須有等於輸入名稱數目的值數量。 例如,如果您的輸入名稱是 "test\_input, expected\_value",則您的第二個自變數看起來可能會像這樣:[(“3+5”, 8),(“3*4”, 12)]
此測試會驗證物件是否具有使用 Python hasattr() 函式的屬性。 它會根據物件是否擁有相關聯的屬性,傳回布爾值。
>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False
由於 hasattr() 需要兩個自變數,因此我們可以以下列方式使用 參數化 :
@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
assert hasattr(item, attribute)
參數化裝飾專案仍會針對第一個自變數使用單一字串,但具有以逗號分隔的兩個自變數名稱,這會成為測試函式的自變數。 在這裡情況下,它是 item 與 attribute。
接下來是兩組項目的清單。 這些對中的每一組都代表要測試的 item 和 attribute。
當 pytest 無法建置傳入之物件的字串表示時,它會建立一個。 執行測試時,您可以看到:
$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 2 items
test_items.py::test_attributes[-format] PASSED [ 50%]
test_items.py::test_attributes[item1-append] PASSED [100%]
============================== 2 passed in 0.01s ===============================