pytest 插件-編寫鉤子函數(shù)

2022-03-29 17:26 更新

鉤子函數(shù)驗(yàn)證和執(zhí)行

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)行。

firstresult:停止在第一個(gè)非無結(jié)果

大多數(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ù)。

hookwrapper:圍繞其他鉤子執(zhí)行

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é)果,但最好避免它。

鉤子函數(shù)ordering/call的例子

對(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í)行順序:

  1. Plugin3的?pytest_collection_modifyitems?被調(diào)用到y(tǒng)ield點(diǎn),因?yàn)樗且粋€(gè)鉤子包裝器。
  2. Plugin1的?pytest_collection_modifyitems?會(huì)被調(diào)用,因?yàn)樗粯?biāo)記為tryfirst=True。
  3. Plugin2的?pytest_collection_modifyitems?被調(diào)用是因?yàn)樗粯?biāo)記為trylast=True(但即使沒有這個(gè)標(biāo)記,它也會(huì)出現(xiàn)在Plugin1之后)。
  4. Plugin3的?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)這些鉤子,以改變行為或與新插件交互:

pytest_addhooks(pluginmanager)

在插件注冊(cè)時(shí)調(diào)用以允許通過調(diào)用 ?pluginmanager.add_hookspecs(module_or_class, prefix)? 添加新的鉤子。

  • 參數(shù):?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)

在 pytest_addoption 中使用鉤子

有時(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ù)安裝的插件有條件地安裝鉤子。

跨鉤子函數(shù)存儲(chǔ)數(shù)據(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?。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)