フック関数の記述

フック関数の検証と実行

pytest は、登録されたプラグインから任意のフックスペックに対してフック関数を呼び出します。 ここでは、すべてのテストアイテムの収集後に pytest が呼び出す pytest_collection_modifyitems(session, config, items) フックの典型的なフック関数を見てみましょう。

プラグイン内で pytest_collection_modifyitems 関数を実装する場合、pytest は登録時に引数名が仕様と一致しているか検証し、一致しなければ処理を中断します。

考えられる実装例を見てみましょう:

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

ここでは、pytestconfig (pytest の設定オブジェクト) と items (収集されたテストアイテムのリスト) を渡しますが、関数シグネチャに session を記載していないため、session 引数は渡されません。 この動的な引数の「剪定」により、pytest は「将来互換性」を保つことが可能になり、新しいフックの名前付きパラメータを既存のフック実装のシグネチャを壊すことなく追加できます。 これが pytest プラグインの互換性が長期間維持される理由の一つです。

pytest_runtest_* 以外のフック関数で例外を投げることは許可されていません。 そうすると pytest の実行が中断します。

firstresult:最初の None でない結果で停止

ほとんどの pytest フック呼び出しは、呼び出されたすべての非 None 結果を含む 結果のリスト を返します。

一部のフックスペックでは firstresult=True オプションが使用され、N 個の登録された関数のうち最初の非 None 結果が返された時点で全体のフック呼び出しの結果としてその値が採用され、残りのフック関数は呼び出されません。

フックラッパー:他のフックの前後で実行

pytest プラグインは、他のフック実装をラップするフックラッパーを実装できます。 フックラッパーは、正確に1度 yield するジェネレーター関数です。 pytest がフックを呼び出すとき、最初にフックラッパーを実行し、通常のフックと同じ引数を渡します。

フックラッパーの yield の箇所で、pytest は次のフック実装を実行し、その結果を yield に返すか、例外が発生した場合はその例外を伝搬させます。

フックラッパーの定義例は次の通りです:

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

フックラッパーはフックのための結果を返すか、例外を発生させる必要があります。

多くの場合、ラッパーは実際のフック実装の周りでトレースや他の副作用を実行するだけでよく、その場合、yield の結果値を返すことができます。 最も単純 (だが無意味) なフックラッパーは return (yield) です。

他の場合では、ラッパーは結果を調整または適応させたい場合があり、その場合、新しい値を返すことができます。 基礎となるフックの結果が可変オブジェクトである場合、ラッパーはその結果を変更することがありますが、避けた方が良いでしょう。

フック実装が例外で失敗した場合、ラッパーは yield の周りで try-catch-finally を使用してその例外を処理し、伝搬させるか、抑制するか、または完全に異なる例外を発生させることができます。

詳細については、フックラッパーに関する pluggy のドキュメント を参照してください。

フック関数の順序 / 呼び出し例

任意のフックスペックには複数の実装が存在する可能性があり、したがって、hook の実行は 1:N の関数呼び出しと見なされます。 ここで N は登録された関数の数です。 フック実装が他の実装の前または後に来るかどうか、つまり 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 ポイントまで呼び出されます。

  2. Plugin1 の pytest_collection_modifyitems は tryfirst=True とマークされているため呼び出されます。

  3. Plugin2 の pytest_collection_modifyitems は trylast=True とマークされているため呼び出されます (ただし、このマークがなくても Plugin1 の後に来ます) 。

  4. その後、Plugin3 の pytest_collection_modifyitems は yield ポイントの後のコードを実行します。 yield は非ラッパーの呼び出しから結果を受け取るか、非ラッパーが例外を発生させた場合は例外を発生させます。

フックラッパーにも tryfirsttrylast を使用することが可能で、その場合、フックラッパー同士の順序に影響を与えます。

新しいフックの宣言

注釈

これは新しいフックの追加方法とその一般的な動作に関する簡単な概要ですが、より完全な概要は pluggy のドキュメント にあります。

プラグインおよび conftest.py ファイルは、新しいフックを宣言することができ、他のプラグインによって実装されて動作を変更したり、新しいプラグインと対話したりすることができます:

pytest_addhooks(pluginmanager)[ソース]

プラグイン登録時に呼び出され、pluginmanager.add_hookspecs(module_or_class, prefix) を呼び出して新しいフックを追加できるようにします。

パラメータ:

pluginmanager (PytestPluginManager) -- pytest プラグインマネージャー。

注釈

このフックはフックラッパーと互換性がありません。

conftest プラグインでの使用

conftest プラグインがこのフックを実装している場合、conftest が登録されるとすぐに呼び出されます。

フックは通常、フックがいつ呼び出されるか、どのような戻り値が期待されるかを説明するドキュメントのみを含む何もしない関数として宣言されます。 関数名は pytest_ で始まらなければなりません。 そうでないと pytest はそれらを認識しません。

ここに例があります。 このコードが sample_hook.py モジュールにあると仮定します。

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

フックを pytest に登録するには、それらを独自のモジュールまたはクラスに構造化する必要があります。 このクラスまたはモジュールは、pytest_addhooks 関数 (これは pytest によって公開されるフック自体) を使用して 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)

実際の例については、xdistnewhooks.py を参照してください。

フックはフィクスチャから、または他のフックから呼び出される場合があります。 いずれの場合も、フックは config オブジェクト内で利用可能な hook オブジェクトを通じて呼び出されます。 ほとんどのフックは直接 config オブジェクトを受け取りますが、フィクスチャは同じオブジェクトを提供する pytestconfig フィクスチャを使用する場合があります。

@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)

注釈

フックはキーワード引数のみを使用してパラメータを受け取ります。

これでフックの準備が整いました。 フックに関数を登録するには、他のプラグインやユーザーは、正しいシグネチャを持つ関数 pytest_my_hookconftest.py に定義するだけです。

例:

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

pytest_addoption でのフックの使用

場合によっては、他のプラグインのフックに基づいて、1つのプラグインによって定義されるコマンドラインオプションの方法を変更する必要があります。 たとえば、プラグインがコマンドラインオプションを公開し、別のプラグインがそのデフォルト値を定義する必要がある場合があります。 pluginmanager を使用してフックをインストールおよび使用してこれを達成できます。 プラグインはフックを定義して追加し、次のように 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"

オプションでサードパーティプラグインのフックを使用する

上記のようにプラグインから新しいフックを使用することは、標準の 検証メカニズム のために少し難しいかもしれません。 インストールされていないプラグインに依存している場合、検証は失敗し、エラーメッセージはユーザーにとって意味をなさないでしょう。

1つのアプローチは、フック関数をプラグインモジュールに直接宣言するのではなく、新しいプラグインにフック実装を延期することです。 たとえば:

# 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())

これには、インストールされているプラグインに応じて条件付きでフックをインストールできるという追加の利点があります。

フック関数間でアイテムにデータを保存する

プラグインは、1つのフック実装で Item にデータを保存し、別のフック実装でそれにアクセスする必要があることがよくあります。 一般的な解決策は、アイテムにいくつかのプライベート属性を直接割り当てることですが、mypy のような型チェッカーはこれに眉をひそめるかもしれませんし、他のプラグインとの競合を引き起こす可能性もあります。 したがって、pytest はこれを行うためのより良い方法を提供します。 item.stash

プラグインで「stash」を使用するには、まずプラグインのトップレベルのどこかに「stash キー」を作成します:

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

次に、キーを使用してデータを一時保存します:

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

スタッシュはすべてのノードタイプ (ClassSession のような) および必要に応じて Config でも利用できます。