撰寫 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-finally
在 yield
周圍處理該例外,方法是傳播它、抑制它或完全引發不同的例外。
如需更多資訊,請參閱 關於 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
...
以下是執行順序
Plugin3 的 pytest_collection_modifyitems 被呼叫,直到 yield 點,因為它是一個 Hook 包裝器。
Plugin1 的 pytest_collection_modifyitems 被呼叫,因為它被標記為
tryfirst=True
。Plugin2 的 pytest_collection_modifyitems 被呼叫,因為它被標記為
trylast=True
(但即使沒有這個標記,它也會在 Plugin1 之後執行)。Plugin3 的 pytest_collection_modifyitems 然後執行 yield 點之後的程式碼。yield 接收來自呼叫非包裝器的結果,或者如果非包裝器引發了例外,則會引發例外。
也可以在 Hook 包裝器上使用 tryfirst
和 trylast
,在這種情況下,它會影響 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!"