撰寫 Hook 函數

Hook 函數驗證和執行

pytest 從已註冊的外掛程式呼叫 Hook 函數,針對任何給定的 Hook 規格。讓我們看看一個典型的 Hook 函數,即 pytest_collection_modifyitems(session, config, items) Hook,pytest 在完成所有測試項目的收集後呼叫它。

當我們在外掛程式中實作 pytest_collection_modifyitems 函數時,pytest 將在註冊期間驗證您使用的引數名稱是否與規格相符,如果不符則會中止。

讓我們看看一個可能的實作

def pytest_collection_modifyitems(config, items):
    # called after collection is completed
    # you can modify the ``items`` list
    ...

在這裡,pytest 將傳入 config (pytest 設定物件) 和 items (收集的測試項目列表),但不會傳入 session 引數,因為我們沒有在函數簽名中列出它。這種動態「修剪」引數的方式使 pytest 能夠「未來相容」:我們可以引入新的 Hook 命名參數,而不會破壞現有 Hook 實作的簽名。這是 pytest 外掛程式通常具有長期相容性的原因之一。

請注意,除了 pytest_runtest_* 之外的 Hook 函數不允許引發例外。這樣做會中斷 pytest 執行。

firstresult:在第一個非 None 結果處停止

大多數對 pytest Hook 的呼叫都會產生結果列表,其中包含所有被呼叫 Hook 函數的非 None 結果。

某些 Hook 規格使用 firstresult=True 選項,以便 Hook 呼叫僅執行到 N 個已註冊函數中的第一個函數返回非 None 結果為止,然後將其作為整個 Hook 呼叫的結果。在這種情況下,其餘的 Hook 函數將不會被呼叫。

Hook 包裝器:在其他 Hook 周圍執行

pytest 外掛程式可以實作 Hook 包裝器,這些包裝器會包裝其他 Hook 實作的執行。Hook 包裝器是一個產生器函數,它只產生一次。當 pytest 調用 Hook 時,它首先執行 Hook 包裝器,並傳遞與常規 Hook 相同的引數。

在 Hook 包裝器的 yield 點,pytest 將執行下一個 Hook 實作,並將其結果返回到 yield 點,或者如果它們引發了例外,則會傳播例外。

以下是 Hook 包裝器的範例定義

import pytest


@pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    # If the outcome is an exception, will raise the exception.
    res = yield

    new_res = post_process_result(res)

    # Override the return value to the plugin system.
    return new_res

Hook 包裝器需要為 Hook 返回結果,或引發例外。

在許多情況下,包裝器只需要在實際的 Hook 實作周圍執行追蹤或其他副作用,在這種情況下,它可以返回 yield 的結果值。最簡單(但無用)的 Hook 包裝器是 return (yield)

在其他情況下,包裝器想要調整或修改結果,在這種情況下,它可以返回一個新值。如果底層 Hook 的結果是可變物件,則包裝器可能會修改該結果,但最好避免這樣做。

如果 Hook 實作因例外而失敗,則包裝器可以使用 try-catch-finallyyield 周圍處理該例外,方法是傳播它、抑制它或完全引發不同的例外。

如需更多資訊,請參閱 關於 Hook 包裝器的 pluggy 文件

Hook 函數排序 / 呼叫範例

對於任何給定的 Hook 規格,可能有多個實作,因此我們通常將 hook 執行視為 1:N 函數呼叫,其中 N 是已註冊函數的數量。有一些方法可以影響 Hook 實作是在其他實作之前還是之後執行,即在 N 大小的函數列表中的位置

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    # will execute as early as possible
    ...


# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    # will execute as late as possible
    ...


# Plugin 3
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    try:
        return (yield)
    finally:
        # will execute after all non-wrappers executed
        ...

以下是執行順序

  1. Plugin3 的 pytest_collection_modifyitems 被呼叫,直到 yield 點,因為它是一個 Hook 包裝器。

  2. Plugin1 的 pytest_collection_modifyitems 被呼叫,因為它被標記為 tryfirst=True

  3. Plugin2 的 pytest_collection_modifyitems 被呼叫,因為它被標記為 trylast=True (但即使沒有這個標記,它也會在 Plugin1 之後執行)。

  4. Plugin3 的 pytest_collection_modifyitems 然後執行 yield 點之後的程式碼。yield 接收來自呼叫非包裝器的結果,或者如果非包裝器引發了例外,則會引發例外。

也可以在 Hook 包裝器上使用 tryfirsttrylast,在這種情況下,它會影響 Hook 包裝器彼此之間的排序。

宣告新的 Hook

注意

這是一個關於如何新增 Hook 以及它們通常如何運作的快速概述,但更完整的概述可以在 pluggy 文件中找到。

外掛程式和 conftest.py 檔案可以宣告新的 Hook,然後可以由其他外掛程式實作,以便更改行為或與新的外掛程式互動

pytest_addhooks(pluginmanager)[來源]

在外掛程式註冊時呼叫,以允許透過呼叫 pluginmanager.add_hookspecs(module_or_class, prefix) 來新增 Hook。

參數:

pluginmanager (PytestPluginManager) – pytest 外掛程式管理器。

注意

此 Hook 與 Hook 包裝器不相容。

在 conftest 外掛程式中使用

如果 conftest 外掛程式實作此 Hook,則在註冊 conftest 時會立即呼叫它。

Hook 通常宣告為無操作函數,僅包含描述何時呼叫 Hook 以及預期的傳回值的文件。函數名稱必須以 pytest_ 開頭,否則 pytest 無法識別它們。

這是一個範例。假設此程式碼位於 sample_hook.py 模組中。

def pytest_my_hook(config):
    """
    Receives the pytest config and does things with it
    """

若要向 pytest 註冊 Hook,它們需要組織在自己的模組或類別中。然後可以使用 pytest_addhooks 函數 (它本身是 pytest 公開的 Hook) 將此類別或模組傳遞給 pluginmanager

def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'sample_hook' module."""
    from my_app.tests import sample_hook

    pluginmanager.add_hookspecs(sample_hook)

如需真實世界的範例,請參閱 xdist 中的 newhooks.py

Hook 可以從 Fixture 或其他 Hook 呼叫。在這兩種情況下,Hook 都是透過 config 物件中可用的 hook 物件呼叫的。大多數 Hook 直接接收 config 物件,而 Fixture 可以使用提供相同物件的 pytestconfig Fixture。

@pytest.fixture()
def my_fixture(pytestconfig):
    # call the hook called "pytest_my_hook"
    # 'result' will be a list of return values from all registered functions.
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

注意

Hook 僅使用關鍵字引數接收參數。

現在您的 Hook 已準備好使用。若要在 Hook 處註冊函數,其他外掛程式或使用者現在只需在其 conftest.py 中定義具有正確簽名的函數 pytest_my_hook 即可。

範例

def pytest_my_hook(config):
    """
    Print all active hooks to the screen.
    """
    print(config.hook)

在 pytest_addoption 中使用 Hook

有時,有必要根據另一個外掛程式中的 Hook 更改一個外掛程式定義命令列選項的方式。例如,一個外掛程式可能會公開一個命令列選項,另一個外掛程式需要為其定義預設值。外掛程式管理器可用於安裝和使用 Hook 來完成此操作。外掛程式將定義和新增 Hook,並按如下方式使用 pytest_addoption

# contents of hooks.py


# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
def pytest_config_file_default_value():
    """Return the default value for the config file command line option."""


# contents of myplugin.py


def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'hooks' module."""
    from . import hooks

    pluginmanager.add_hookspecs(hooks)


def pytest_addoption(parser, pluginmanager):
    default_value = pluginmanager.hook.pytest_config_file_default_value()
    parser.addoption(
        "--config-file",
        help="Config file to use, defaults to %(default)s",
        default=default_value,
    )

使用 myplugin 的 conftest.py 將簡單地按如下方式定義 Hook

def pytest_config_file_default_value():
    return "config.yaml"

選擇性地使用來自協力廠商外掛程式的 Hook

使用如上所述來自外掛程式的新 Hook 可能有點棘手,因為標準的 驗證機制:如果您依賴未安裝的外掛程式,驗證將失敗,並且錯誤訊息對您的使用者來說沒有太多意義。

一種方法是將 Hook 實作延遲到一個新的外掛程式,而不是直接在外掛程式模組中宣告 Hook 函數,例如

# contents of myplugin.py


class DeferPlugin:
    """Simple plugin to defer pytest-xdist hook functions."""

    def pytest_testnodedown(self, node, error):
        """standard xdist hook function."""


def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

這還具有允許您根據安裝了哪些外掛程式有條件地安裝 Hook 的額外好處。

跨 Hook 函數在項目上儲存資料

外掛程式通常需要在一個 Hook 實作中在 Item 上儲存資料,並在另一個 Hook 實作中存取它。一種常見的解決方案是直接在項目上分配一些私有屬性,但像 mypy 這樣的類型檢查器對此表示不贊同,並且它也可能導致與其他外掛程式衝突。因此,pytest 提供了一種更好的方法來做到這一點,即 item.stash

若要在您的外掛程式中使用「stash」,請先在您的外掛程式的頂層某處建立「stash 鍵」

been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()

然後在某個點使用鍵來 stash 您的資料

def pytest_runtest_setup(item: pytest.Item) -> None:
    item.stash[been_there_key] = True
    item.stash[done_that_key] = "no"

並在另一個點檢索它們

def pytest_runtest_teardown(item: pytest.Item) -> None:
    if not item.stash[been_there_key]:
        print("Oh?")
    item.stash[done_that_key] = "yes!"

Stash 在所有節點類型 (如 ClassSession) 以及 Config 上可用 (如果需要)。