pytest-2.3:fixture/funcarg 演進的原因

目標受眾:閱讀本文檔需要 Python 測試、xUnit 設定方法和 (先前) 基本 pytest funcarg 機制的基礎知識,請參閱 funcargs 和 pytest_funcarg__。如果您是 pytest 的新手,則可以忽略本節並閱讀其他章節。

先前 pytest_funcarg__ 機制的缺點

在 pytest-2.3 之前的 funcarg 機制中,每次需要測試函數的 funcarg 時,都會調用工廠函數。如果工廠想要跨不同範圍重用資源,它通常使用 request.cached_setup() 輔助函數來管理資源的快取。以下是一個基本範例,說明我們如何實作每個 session 的 Database 物件

# content of conftest.py
class Database:
    def __init__(self):
        print("database instance created")

    def destroy(self):
        print("database instance destroyed")


def pytest_funcarg__db(request):
    return request.cached_setup(
        setup=DataBase, teardown=lambda db: db.destroy, scope="session"
    )

這種方法存在一些限制和困難

  1. 設定 funcarg 資源建立的範圍並不直接,而是必須理解複雜的 cached_setup() 方法機制。

  2. 參數化 “db” 資源並不直接:您需要套用 “parametrize” 裝飾器或實作呼叫 pytest_generate_tests hook 的 parametrize() hook,該 hook 在資源使用的地方執行參數化。此外,您需要修改工廠以使用包含 request.paramextrakey 參數來呼叫 Request.cached_setup

  3. 多個參數化的 session 範圍資源將同時處於活動狀態,使得它們難以影響被測應用程式的全局狀態。

  4. 您無法在 xUnit 設定方法中使用 funcarg 工廠。

  5. 如果非參數化的 fixture 函數未在測試函數簽名中聲明,則無法使用參數化的 funcarg 資源。

所有這些限制都已在 pytest-2.3 及其改進的 fixture 機制 中得到解決。

直接設定 fixture/funcarg 工廠的範圍

您可以使用 @pytest.fixture 裝飾器直接聲明範圍,而不是使用快取範圍呼叫 cached_setup()

@pytest.fixture(scope="session")
def db(request):
    # factory will only be invoked once per session -
    db = DataBase()
    request.addfinalizer(db.destroy)  # destroy when session is finished
    return db

此工廠實作不再需要呼叫 cached_setup(),因為它每個 session 只會被調用一次。此外,request.addfinalizer() 會根據工廠函數運作的指定資源範圍註冊一個終結器。

直接參數化 funcarg 資源工廠

以前,funcarg 工廠無法直接引起參數化。您需要在測試函數上指定 @parametrize 裝飾器或實作 pytest_generate_tests hook 來執行參數化,即使用不同的值集多次呼叫測試。pytest-2.3 引入了一個裝飾器,用於工廠本身

@pytest.fixture(params=["mysql", "pg"])
def db(request): ...  # use request.param

在這裡,工廠將被調用兩次(分別使用設定為 request.param 屬性的 “mysql” 和 “pg” 值),並且所有需要 “db” 的測試也將執行兩次。“mysql” 和 “pg” 值也將用於報告測試調用變體。

這種新的參數化 funcarg 工廠的方式在許多情況下應該允許重用已編寫的工廠,因為當通過 metafunc.parametrize(indirect=True) 呼叫參數化測試函數/類別時,實際上已經使用了 request.param

當然,完全可以結合參數化和範圍設定

@pytest.fixture(scope="session", params=["mysql", "pg"])
def db(request):
    if request.param == "mysql":
        db = MySQL()
    elif request.param == "pg":
        db = PG()
    request.addfinalizer(db.destroy)  # destroy when session is finished
    return db

這將執行所有需要每個 session “db” 資源的測試兩次,接收由工廠函數的兩個相應調用建立的值。

使用 @fixture 裝飾器時,沒有 pytest_funcarg__ 前綴

使用 @fixture 裝飾器時,函數的名稱表示可以作為函數參數存取資源的名稱

@pytest.fixture()
def db(request): ...

可以請求 funcarg 資源的名稱是 db

您仍然可以使用指定 funcarg 工廠的 “舊” 非裝飾器方式,也就是

def pytest_funcarg__db(request): ...

但是這樣就無法定義範圍設定和參數化。因此,建議使用工廠裝飾器。

解決每個 session 的設定 / autouse fixtures

長期以來,pytest 提供了 pytest_configure 和 pytest_sessionstart hook,它們通常用於設定全局資源。這存在幾個問題

  1. 在分散式測試中,管理進程會設定永遠不需要的測試資源,因為它只協調 worker 進程的測試執行活動。

  2. 如果您只執行收集 (“–collect-only”),仍然會執行資源設定。

  3. 如果 pytest_sessionstart 包含在某些子目錄 conftest.py 檔案中,則不會調用它。這源於此 hook 實際上用於報告,特別是包含平台/自訂資訊的測試標頭。

此外,從外掛程式或 conftest 檔案中定義範圍設定並不容易,除非實作 pytest_runtest_setup() hook 並自行處理範圍設定/快取。並且幾乎不可能通過參數化來完成此操作,因為 pytest_runtest_setup() 在測試執行期間調用,而參數化發生在收集時。

因此,pytest_configure/session/runtest_setup 通常不適合實作常見的 fixture 需求。因此,pytest-2.3 引入了 Autouse fixtures(您不必請求的 fixtures),它與通用的 fixture 機制 完全整合,並且淘汰了許多先前 pytest hook 的用法。

funcargs/fixture 發現現在在收集時發生

自 pytest-2.3 以來,fixture/funcarg 工廠的發現會在收集時處理。這對於大型測試套件來說更有效率。此外,調用 “pytest –collect-only” 在未來應該能夠顯示大量設定資訊,因此提供了一種很好的方法來了解專案中的 fixture 管理。

結論和相容性注意事項

funcargs 最初在 pytest-2.0 中引入。在 pytest-2.3 中,該機制得到了擴展和改進,現在被描述為 fixtures

  • 以前,funcarg 工廠使用特殊的 pytest_funcarg__NAME 前綴指定,而不是使用 @pytest.fixture 裝飾器。

  • 工廠接收到一個 request 物件,該物件通過 request.cached_setup() 呼叫管理快取,並允許通過 request.getfuncargvalue() 呼叫使用其他 funcargs。這些複雜的 API 使得難以進行適當的參數化和實作資源快取。新的 pytest.fixture() 裝飾器允許宣告範圍,並讓 pytest 為您找出解決方案。

  • 如果您使用了參數化和 funcarg 工廠,並且使用了 request.cached_setup(),建議您花費幾分鐘時間簡化您的 fixture 函數程式碼,以改用 Fixtures 參考 裝飾器。這也將允許您利用測試的自動每個資源分組。