pytest 從注冊(cè)插件中調(diào)用任何給定鉤子規(guī)范的鉤子函數(shù)。 讓我們看一下 ?pytest_collection_modifyitems(session, config, items)
? 鉤子的典型鉤子函數(shù),pytest 在完成所有測試項(xiàng)的收集后調(diào)用該鉤子。
當(dāng)我們?cè)诓寮袑?shí)現(xiàn) ?pytest_collection_modifyitems
? 函數(shù)時(shí),pytest 將在注冊(cè)期間驗(yàn)證您使用的參數(shù)名稱是否與規(guī)范匹配,如果不匹配則退出。
讓我們看一個(gè)可能的實(shí)現(xiàn):
def pytest_collection_modifyitems(config, items):
# called after collection is completed
# you can modify the ``items`` list
...
這里,pytest將傳入?config
?(pytest配置對(duì)象)和?items
?(收集的測試項(xiàng)列表),但不會(huì)傳入?session
?參數(shù),因?yàn)槲覀儧]有在函數(shù)簽名中列出它。這種參數(shù)的動(dòng)態(tài)修剪允許pytest與未來兼容:我們可以引入新的命名為鉤子的參數(shù),而不會(huì)破壞現(xiàn)有鉤子實(shí)現(xiàn)的簽名。這也是pytest插件長期兼容的原因之一。
注意,除?pytest_runtest_*
?外的鉤子函數(shù)不允許拋出異常。這樣做將破壞pytest的運(yùn)行。
大多數(shù)對(duì) pytest 鉤子的調(diào)用都會(huì)產(chǎn)生一個(gè)結(jié)果列表,其中包含被調(diào)用鉤子函數(shù)的所有非無結(jié)果。
一些鉤子規(guī)范使用 ?firstresult=True
? 選項(xiàng),因此鉤子調(diào)用只執(zhí)行,直到 ?N
個(gè)注冊(cè)函數(shù)中的第一個(gè)返回非無結(jié)果,然后將其作為整個(gè)鉤子調(diào)用的結(jié)果。 在這種情況下,不會(huì)調(diào)用剩余的鉤子函數(shù)。
Pytest插件可以實(shí)現(xiàn)鉤子包裝器來包裝其他鉤子實(shí)現(xiàn)的執(zhí)行。鉤子包裝器是一個(gè)生成器函數(shù),它只生成一次。當(dāng)pytest調(diào)用鉤子時(shí),它首先執(zhí)行鉤子包裝器,并傳遞與常規(guī)鉤子相同的參數(shù)。
在鉤子包裝器的?yield
點(diǎn),pytest將執(zhí)行下一個(gè)鉤子實(shí)現(xiàn),并將它們的結(jié)果以?result
實(shí)例的形式返回給?yield
?點(diǎn),該實(shí)例封裝了一個(gè)結(jié)果或異常信息。因此,yield點(diǎn)本身通常不會(huì)引發(fā)異常(除非有bug)。
下面是一個(gè)鉤子包裝器的定義示例:
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
do_something_before_next_hook_executes()
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
res = outcome.get_result() # will raise if outcome was exception
post_process_result(res)
outcome.force_result(new_res) # to override the return value to the plugin system
請(qǐng)注意,鉤子包裝器本身不會(huì)返回結(jié)果,它們只是圍繞實(shí)際的鉤子實(shí)現(xiàn)執(zhí)行跟蹤或其他副作用。 如果底層鉤子的結(jié)果是一個(gè)可變對(duì)象,他們可能會(huì)修改該結(jié)果,但最好避免它。
對(duì)于任何給定的鉤子規(guī)范,都可能有多個(gè)實(shí)現(xiàn),因此我們通常將鉤子的執(zhí)行視為?1:N
?的函數(shù)調(diào)用,其中?N
?是注冊(cè)函數(shù)的數(shù)量。有幾種方法可以影響一個(gè)鉤子實(shí)現(xiàn)是在其他實(shí)現(xiàn)之前還是之后,即在?n
?個(gè)函數(shù)列表中的位置:
# 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(hookwrapper=True)
def pytest_collection_modifyitems(items):
# will execute even before the tryfirst one above!
outcome = yield
# will execute after all non-hookwrappers executed
以下是執(zhí)行順序:
pytest_collection_modifyitems
?被調(diào)用到y(tǒng)ield點(diǎn),因?yàn)樗且粋€(gè)鉤子包裝器。pytest_collection_modifyitems
?會(huì)被調(diào)用,因?yàn)樗粯?biāo)記為tryfirst=True。pytest_collection_modifyitems
?被調(diào)用是因?yàn)樗粯?biāo)記為trylast=True(但即使沒有這個(gè)標(biāo)記,它也會(huì)出現(xiàn)在Plugin1之后)。pytest_collection_modifyitems
?,然后在yield點(diǎn)之后執(zhí)行代碼。yield接收到一個(gè)Result實(shí)例,該實(shí)例通過調(diào)用非包裝器封裝了結(jié)果。包裝器不得修改結(jié)果。也可以將 ?tryfirst
和 ?trylast
與 ?hookwrapper=True
結(jié)合使用,在這種情況下,它會(huì)影響 ?hookwrapper
之間的順序。
插件和 ?conftest.py
? 文件可以聲明新的鉤子,然后其他插件可以實(shí)現(xiàn)這些鉤子,以改變行為或與新插件交互:
在插件注冊(cè)時(shí)調(diào)用以允許通過調(diào)用 ?pluginmanager.add_hookspecs(module_or_class, prefix)
? 添加新的鉤子。
pluginmanager
(pytest.PytestPluginManager) – The pytest plugin manager.None
?這個(gè)鉤子與 ?hookwrapper=True
? 不兼容。
鉤子通常被聲明為無操作函數(shù),其中僅包含描述何時(shí)調(diào)用鉤子以及預(yù)期返回值的文檔。 函數(shù)的名稱必須以 ?pytest_
? 開頭,否則 pytest 將無法識(shí)別它們。
這是一個(gè)例子。 假設(shè)這段代碼在 ?sample_hook.py
? 模塊中。
def pytest_my_hook(config):
"""
Receives the pytest config and does things with it
"""
要使用 pytest 注冊(cè)鉤子,它們需要在自己的模塊或類中構(gòu)建。 然后可以使用 ?pytest_addhooks
函數(shù)(它本身是 pytest 公開的鉤子)將此類或模塊傳遞給插件管理器。
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)
鉤子可以從?fixture
?中調(diào)用,也可以從其他鉤子中調(diào)用。在這兩種情況下,鉤子都是通過配置對(duì)象中可用的鉤子對(duì)象調(diào)用的。大多數(shù)鉤子直接接收配置對(duì)象,而?fixture
?可以使用提供相同對(duì)象的?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)
鉤子僅使用關(guān)鍵字參數(shù)接收參數(shù)。
現(xiàn)在你的鉤子已經(jīng)可以使用了。 要在鉤子上注冊(cè)一個(gè)函數(shù),其他插件或用戶現(xiàn)在必須簡單地在其 ?conftest.py
? 中使用正確的簽名定義函數(shù) ?pytest_my_hook
?。
例如:
def pytest_my_hook(config):
"""
Print all active hooks to the screen.
"""
print(config.hook)
有時(shí)候,有必要改變一個(gè)插件基于另一個(gè)插件中的鉤子定義命令行選項(xiàng)的方式。例如,一個(gè)插件可能暴露一個(gè)命令行選項(xiàng),而另一個(gè)插件需要為該選項(xiàng)定義默認(rèn)值。插件管理器可以用來安裝和使用鉤子來完成這個(gè)任務(wù)。插件將定義和添加鉤子,并使用?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
? 將簡單地定義鉤子,如下所示:
def pytest_config_file_default_value():
return "config.yaml"
因?yàn)闃?biāo)準(zhǔn)的驗(yàn)證機(jī)制,從上面解釋的插件中使用新的鉤子可能有點(diǎn)棘手:如果你依賴于一個(gè)沒有安裝的插件,驗(yàn)證將會(huì)失敗,錯(cuò)誤消息對(duì)你的用戶也沒有多大意義。
一種方法是將鉤子實(shí)現(xiàn)延遲到一個(gè)新的插件,而不是直接在你的插件模塊中聲明鉤子函數(shù),例如:
# 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())
這有一個(gè)額外的好處,允許你根據(jù)安裝的插件有條件地安裝鉤子。
插件通常需要在一個(gè)鉤子實(shí)現(xiàn)中存儲(chǔ)?Items
上的數(shù)據(jù),然后在另一個(gè)鉤子實(shí)現(xiàn)中訪問它。一個(gè)常見的解決方案是直接在項(xiàng)目上分配一些私有屬性,但是像?mypy
?這樣的類型檢查器不贊成這樣做,而且它還可能導(dǎo)致與其他插件的沖突。所以pytest提供了一種更好的方法,?item.stash
?
要在插件中使用?stash
?,首先要在插件的頂層某處創(chuàng)建?stash keys
?:
been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()
然后在某個(gè)時(shí)候使用密鑰存儲(chǔ)您的數(shù)據(jù):
def pytest_runtest_setup(item: pytest.Item) -> None:
item.stash[been_there_key] = True
item.stash[done_that_key] = "no"
然后在另一個(gè)點(diǎn)檢索它們:
def pytest_runtest_teardown(item: pytest.Item) -> None:
if not item.stash[been_there_key]:
print("Oh?")
item.stash[done_that_key] = "yes!"
在所有節(jié)點(diǎn)類型(如?Class
?、?Session
?)和?Config
?(如果需要的話)上都可以使用?stash
?。
更多建議: