Pytest 基本概念

已完成

開始使用 Pytest 執行測試吧。 如上一個單元所述,Pytest 具有高度可設定性,而且可以處理複雜的測試套件,但不需要太多才能開始撰寫測試。 事實上,能讓您輕鬆撰寫測試的架構是再好不過了。

本課程結束後,您應該已經有開始撰寫第一個測試所需的所有項目,並使用 Pytest 執行測試。

慣例

在深入介紹如何撰寫測試之前,我們必須先瞭解 Pytest 使用的一些測試慣例。

Python 中測試檔案、測試目錄或一般測試版面配置沒有太多硬式規則。 瞭解這些規則後,您就可以使用自動測試探索和執行功能,且不需任何額外的設定。

測試目錄和測試檔案

測試的主要目錄就是「tests」目錄。 您可以將此目錄放在專案的根層級,但放在程式碼模組旁也很常見。

注意

在本課程模組中,我們預設會在專案的根目錄中使用 測試

來看看名為「jformat」的小型 Python 專案根目錄會如何顯示吧:

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

tests」目錄位於內含單一測試檔案的專案根目錄。 在此案例中,我們將測試檔案命名為「test_main.py」。 此範例示範兩個重要慣例:

  • 使用 [tests] 目錄放置測試檔案和巢狀測試目錄。
  • 使用「test」作為測試檔案的前置詞。 前置詞表示該檔案內含測試程式碼。

警告

請勿使用「test」(單數) 作為目錄名稱。 「test」名稱是 Python 模組,因此建立同名目錄後,該檔案就會被覆寫。 請一律改為使用複數型 tests

測試函式

使用 Pytest 的強大論點是它允許您撰寫測試函式。 與測試檔案類似,測試函式必須以「test_」作為前置詞。 「test_」前置詞可確保 Pytest 會收集測試並加以執行。

以下是簡單測試函式看起來的樣子:

def test_main():
    assert "a string value" == "a string value"

注意

如果您熟悉 unittest,在測試函式中看到 assert 的用法可能會出人意料。 我們稍後會更詳細地介紹一般判斷提示,但使用 Pytest 後,您將收到有關一般判斷提示的豐富失敗報告。

測試類別和測試方法

與檔案和函式的慣例類似,測試類別和測試方法會使用下列慣例:

  • 測試類別以「Test」作為前置詞
  • 測試方法以「test_」作為前置詞

與 Python unittest 程式庫的核心差異是不需進行任何繼承。

下列範例會針對類別和方法使用這些前置詞和其他 Python 命名慣例。 它示範正在檢查應用程式中使用者名稱的小型測試類別。

class TestUser:

    def test_username(self):
        assert default() == "default username"

執行測試

Pytest 既是測試架構,也是測試執行器。 測試執行器是在命令列中運行的可執行檔,其主要功能包括:

  • 尋找測試回合的所有測試檔案、測試類別和測試函式,方便您執行測試集合。
  • 執行所有測試,方便您開始測試回合。
  • 追蹤失敗、錯誤和已通過的測試。
  • 在測試回合結束時提供內容豐富的報告。

注意

因為 Pytest 是外部連結庫,所以 必須 安裝才能使用它。

我們可以透過「test_main.py」檔案中的內容瞭解 Pytest 如何執行測試:

# contents of test_main.py file

def test_main():
    assert True

我們可以在命令列中,與「test_main.py」檔案所在的相同路徑上執行可執行檔 pytest

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

Pytest 會在幕後收集測試檔案中的範例測試,而不需進行任何設定。

強大的判斷提示陳述式

到目前為止,我們的測試範例全都使用一般 assert 呼叫。 通常,在 Python 中, assert 語句不會用於測試,因為它在判斷提示失敗時缺少適當的報告。 不過,Pytest 沒有這個限制。 在幕後,Pytest 可讓 語句執行豐富的比較,而不需要強制使用者撰寫更多程式代碼或設定任何專案。

藉由使用純 assert 文字語句,您可以使用 Python 的運算元;例如、 ><!=>=<=。 所有 Python 運算子都是有效的。 這可能是 Pytest 最重要的功能:您不需學習新的語法就能撰寫判斷提示。

我們來看看這會對使用 Python 物件處理一般比較造成什麼影響。 在此案例中,讓我們仔細檢查比較長字串時的失敗報告:

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

Pytest 會顯示失敗的相關實用內容:字串開頭的不正確大小寫,以及單字中的額外字元。 但除了字串之外,Pytest 也能協助其他物件和資料結構。 例如,以下是它針對清單的運作模式:

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

此報告會識別出索引 1 (即清單中的第二個項目) 不同。 它不僅會識別索引編號,還會提供失敗情況的表示法。 除了項目比較之外,它也可以回報是否有項目遺漏並提供相關資訊,方便您瞭解遺漏的究竟是哪些項目。 在下列案例中,就是 "milk"

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

最後,我們來看看針對字典的運作模式。 如果發生失敗情況,要比較兩個大型字典就很困難,但 Pytest 十分擅長提供內容並找出失敗情況:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

在此測試中,字典裡有兩處失敗。 其中一處是 "street" 為不同值,另一處則是 "number" 不相符。

Pytest 準確地偵測到這些差異(即使這是單一測試中的一個失敗)。 因為字典包含許多項目,Pytest 會省略相同的部分,並且只顯示相關的內容。 我們來看看如果使用建議的 -vv 旗標來提升輸出的詳細程度,究竟會發生什麼事:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

執行 pytest -vv 可讓報告中的資訊更詳細,並提供精細的比較。 此報告不僅會偵測並顯示失敗情況,還可讓您快速進行變更以針對問題進行補救。