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

2022-03-29 17:26 更新

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

pytest 從注冊插件中調用任何給定鉤子規(guī)范的鉤子函數(shù)。 讓我們看一下 ?pytest_collection_modifyitems(session, config, items)? 鉤子的典型鉤子函數(shù),pytest 在完成所有測試項的收集后調用該鉤子。

當我們在插件中實現(xiàn) ?pytest_collection_modifyitems? 函數(shù)時,pytest 將在注冊期間驗證您使用的參數(shù)名稱是否與規(guī)范匹配,如果不匹配則退出。

讓我們看一個可能的實現(xiàn):

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

這里,pytest將傳入?config ?(pytest配置對象)和?items?(收集的測試項列表),但不會傳入?session?參數(shù),因為我們沒有在函數(shù)簽名中列出它。這種參數(shù)的動態(tài)修剪允許pytest與未來兼容:我們可以引入新的命名為鉤子的參數(shù),而不會破壞現(xiàn)有鉤子實現(xiàn)的簽名。這也是pytest插件長期兼容的原因之一。

注意,除?pytest_runtest_*?外的鉤子函數(shù)不允許拋出異常。這樣做將破壞pytest的運行。

firstresult:停止在第一個非無結果

大多數(shù)對 pytest 鉤子的調用都會產生一個結果列表,其中包含被調用鉤子函數(shù)的所有非無結果。

一些鉤子規(guī)范使用 ?firstresult=True? 選項,因此鉤子調用只執(zhí)行,直到 ?N個注冊函數(shù)中的第一個返回非無結果,然后將其作為整個鉤子調用的結果。 在這種情況下,不會調用剩余的鉤子函數(shù)。

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

Pytest插件可以實現(xiàn)鉤子包裝器來包裝其他鉤子實現(xiàn)的執(zhí)行。鉤子包裝器是一個生成器函數(shù),它只生成一次。當pytest調用鉤子時,它首先執(zhí)行鉤子包裝器,并傳遞與常規(guī)鉤子相同的參數(shù)。

在鉤子包裝器的?yield點,pytest將執(zhí)行下一個鉤子實現(xiàn),并將它們的結果以?result實例的形式返回給?yield?點,該實例封裝了一個結果或異常信息。因此,yield點本身通常不會引發(fā)異常(除非有bug)。

下面是一個鉤子包裝器的定義示例:

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

請注意,鉤子包裝器本身不會返回結果,它們只是圍繞實際的鉤子實現(xiàn)執(zhí)行跟蹤或其他副作用。 如果底層鉤子的結果是一個可變對象,他們可能會修改該結果,但最好避免它。

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

對于任何給定的鉤子規(guī)范,都可能有多個實現(xiàn),因此我們通常將鉤子的執(zhí)行視為?1:N?的函數(shù)調用,其中?N?是注冊函數(shù)的數(shù)量。有幾種方法可以影響一個鉤子實現(xiàn)是在其他實現(xiàn)之前還是之后,即在?n?個函數(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?被調用到y(tǒng)ield點,因為它是一個鉤子包裝器。
  2. Plugin1的?pytest_collection_modifyitems?會被調用,因為它被標記為tryfirst=True。
  3. Plugin2的?pytest_collection_modifyitems?被調用是因為它被標記為trylast=True(但即使沒有這個標記,它也會出現(xiàn)在Plugin1之后)。
  4. Plugin3的?pytest_collection_modifyitems?,然后在yield點之后執(zhí)行代碼。yield接收到一個Result實例,該實例通過調用非包裝器封裝了結果。包裝器不得修改結果。

也可以將 ?tryfirst和 ?trylast與 ?hookwrapper=True 結合使用,在這種情況下,它會影響 ?hookwrapper之間的順序。

聲明新的鉤子

插件和 ?conftest.py? 文件可以聲明新的鉤子,然后其他插件可以實現(xiàn)這些鉤子,以改變行為或與新插件交互:

pytest_addhooks(pluginmanager)

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

  • 參數(shù):?pluginmanager(pytest.PytestPluginManager) – The pytest plugin manager.
  • 返回類型:?None?

這個鉤子與 ?hookwrapper=True? 不兼容。

鉤子通常被聲明為無操作函數(shù),其中僅包含描述何時調用鉤子以及預期返回值的文檔。 函數(shù)的名稱必須以 ?pytest_? 開頭,否則 pytest 將無法識別它們。

這是一個例子。 假設這段代碼在 ?sample_hook.py? 模塊中。

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

要使用 pytest 注冊鉤子,它們需要在自己的模塊或類中構建。 然后可以使用 ?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?中調用,也可以從其他鉤子中調用。在這兩種情況下,鉤子都是通過配置對象中可用的鉤子對象調用的。大多數(shù)鉤子直接接收配置對象,而?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)

鉤子僅使用關鍵字參數(shù)接收參數(shù)。

現(xiàn)在你的鉤子已經可以使用了。 要在鉤子上注冊一個函數(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 中使用鉤子

有時候,有必要改變一個插件基于另一個插件中的鉤子定義命令行選項的方式。例如,一個插件可能暴露一個命令行選項,而另一個插件需要為該選項定義默認值。插件管理器可以用來安裝和使用鉤子來完成這個任務。插件將定義和添加鉤子,并使用?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"

可以選擇使用來自第三方插件的鉤子

因為標準的驗證機制,從上面解釋的插件中使用新的鉤子可能有點棘手:如果你依賴于一個沒有安裝的插件,驗證將會失敗,錯誤消息對你的用戶也沒有多大意義。

一種方法是將鉤子實現(xiàn)延遲到一個新的插件,而不是直接在你的插件模塊中聲明鉤子函數(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())

這有一個額外的好處,允許你根據(jù)安裝的插件有條件地安裝鉤子。

跨鉤子函數(shù)存儲數(shù)據(jù)

插件通常需要在一個鉤子實現(xiàn)中存儲?Items上的數(shù)據(jù),然后在另一個鉤子實現(xiàn)中訪問它。一個常見的解決方案是直接在項目上分配一些私有屬性,但是像?mypy?這樣的類型檢查器不贊成這樣做,而且它還可能導致與其他插件的沖突。所以pytest提供了一種更好的方法,?item.stash?

要在插件中使用?stash?,首先要在插件的頂層某處創(chuàng)建?stash keys?:

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

然后在某個時候使用密鑰存儲您的數(shù)據(jù):

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!"

在所有節(jié)點類型(如?Class?、?Session?)和?Config?(如果需要的話)上都可以使用?stash?。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號