プラグインの作成

自分のプロジェクト用の ローカル conftest プラグイン や、サードパーティプロジェクトを含む多くのプロジェクトで使用できる pip インストール可能なプラグイン を簡単に実装できます。 プラグインを使用するだけで作成しない場合は、プラグインのインストールと使用方法 を参照してください。

プラグインには1つまたは複数のフック関数が含まれます。 Writing hooks では、フック関数を自分で書く方法の基本と詳細を説明しています。 pytest は、次のプラグインの well specified hooks を呼び出すことで、設定、収集、実行、およびレポートのすべての側面を実装します。

  • 組み込みプラグイン: pytest の内部 _pytest ディレクトリから読み込まれます。

  • external plugins: パッケージメタデータの entry points を通じて発見されたインストール済みのサードパーティモジュール

  • conftest.py plugins: テストディレクトリで自動的に発見されるモジュール

原則として、各フック呼び出しは 1:N の Python 関数呼び出しであり、N は特定の仕様に対して登録された実装関数の数です。 すべての仕様と実装は pytest_ プレフィックス命名規則に従っているため、区別して見つけやすくなっています。

ツール起動時のプラグイン発見順序

pytest はツール起動時に次の方法でプラグインモジュールを読み込みます:

  1. コマンドラインをスキャンして -p no:name オプションを探し、そのプラグインの読み込みを*ブロック*します (組み込みプラグインでもこの方法でブロックできます) 。 これは通常のコマンドライン解析の前に行われます。

  2. すべての組み込みプラグインを読み込みます。

  3. コマンドラインをスキャンして -p name オプションを探し、指定されたプラグインを読み込みます。 これは通常のコマンドライン解析の前に行われます。

  4. PYTEST_DISABLE_PLUGIN_AUTOLOAD 環境変数が設定されていない限り、インストールされたサードパーティパッケージの entry points を通じて登録されたすべてのプラグインを読み込みます。

  5. PYTEST_PLUGINS 環境変数を通じて指定されたすべてのプラグインを読み込みます。

  6. すべての "initial "conftest.py ファイルを読み込みます:

    • テストパスを決定します: コマンドラインで指定された場合、または testpaths に定義されている場合は rootdir から実行し、それ以外の場合は現在のディレクトリ

    • 各テストパスに対して、テストパスのディレクトリ部分に相対的な conftest.py および test*/conftest.py を読み込みます (存在する場合) 。 conftest.py ファイルが読み込まれる前に、その親ディレクトリ内のすべての conftest.py ファイルを読み込みます。 conftest.py ファイルが読み込まれた後、その pytest_plugins 変数に指定されたすべてのプラグインを再帰的に読み込みます (存在する場合) 。

conftest.py: ディレクトリごとのローカルプラグイン

ローカルの conftest.py プラグインには、ディレクトリ固有のフック実装が含まれています。 フックセッションおよびテスト実行アクティビティは、ファイルシステムのルートに近い conftest.py ファイルに定義されたすべてのフックを呼び出します。 a サブディレクトリのテストに対してのみ呼び出され、他のディレクトリには呼び出されない pytest_runtest_setup フックの実装例:

a/conftest.py:
    def pytest_runtest_setup(item):
        # called for running each test in 'a' directory
        print("setting up", item)

a/test_sub.py:
    def test_sub():
        pass

test_flat.py:
    def test_flat():
        pass

次のように実行できます:

pytest test_flat.py --capture=no  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "setting up"

注釈

conftest.py ファイルが Python パッケージディレクトリ (つまり、__init__.py を含むディレクトリ) に存在しない場合、「import conftest」は曖昧になる可能性があります。 なぜなら、PYTHONPATHsys.path に他の conftest.py ファイルが存在する可能性があるからです。 したがって、プロジェクトでは conftest.py をパッケージスコープに置くか、conftest.py ファイルから何もインポートしないことが良いプラクティスです。

参照: pytest のインポートメカニズムと sys.path/PYTHONPATH

注釈

pytest が起動時にプラグインを発見する方法のため、initial でない conftest.py ファイルでは実装できないフックもあります。 詳細は各フックのドキュメントを参照してください。

独自のプラグインの作成

プラグインを作成したい場合、コピーできる実際の例がたくさんあります:

これらのプラグインはすべて、機能を拡張および追加するために hooks および/または fixtures を実装しています。

注釈

プラグイン作成のための cookiecutter template である、優れた cookiecutter-pytest-plugin プロジェクトをぜひチェックしてください。

このテンプレートは、動作するプラグイン、tox で実行されるテスト、包括的な README ファイル、および事前に設定されたエントリーポイントを備えた優れた出発点を提供します。

自分以外に満足しているユーザーがいる場合は、contributing your plugin to pytest-dev も検討してください。

他の人がプラグインをインストールできるようにする

プラグインを外部で利用可能にしたい場合、ディストリビューションのエントリーポイントを定義して、pytest がプラグインモジュールを見つけられるようにすることができます。 エントリーポイントは packaging tools によって提供される機能です。

pytest は pytest11 エントリーポイントを調べてプラグインを発見するため、pyproject.toml ファイルに定義することでプラグインを利用可能にできます。

# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "myproject"
classifiers = [
    "Framework :: Pytest",
]

[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"

この方法でパッケージがインストールされると、pytestmyproject.pluginmodule をプラグインとして読み込み、hooks を定義できます。 pytest --trace-config で登録を確認してください。

注釈

ユーザーがプラグインを見つけやすくするために、PyPI classifiers のリストに Framework :: Pytest を含めるようにしてください。

アサーションの書き換え

pytest の主な機能の1つは、単純なアサート文の使用と、アサーション失敗時の式の詳細な内省です。 これは「アサーションの書き換え」によって提供され、解析された AST をバイトコードにコンパイルする前に修正します。 これは PEP 302 インポートフックを介して行われ、pytest が起動すると早期にインストールされ、モジュールがインポートされるときにこの書き換えを実行します。 ただし、実運用で実行するバイトコードとは異なるものをテストしたくないため、このフックはテストモジュール自体 (python_files 設定オプションで定義されている) およびプラグインの一部であるモジュールのみを書き換えます。 他のインポートされたモジュールは書き換えられず、通常のアサーション動作が行われます。

他のモジュールにアサーションヘルパーがあり、アサーションの書き換えを有効にする必要がある場合は、インポートされる前に pytest に明示的にこのモジュールを書き換えるように依頼する必要があります。

register_assert_rewrite(*names)[ソース]

インポート時に書き換えるモジュール名を1つ以上登録します。

この関数は、このモジュールまたはパッケージ内のすべてのモジュールのアサート文が書き換えられることを保証します。 したがって、モジュールが実際にインポートされる前に、通常はパッケージを使用するプラグインの __init__.py でこれを呼び出すようにしてください。

パラメータ:

names (str) -- 登録するモジュール名。

これは、パッケージを使用して作成された pytest プラグインを書くときに特に重要です。 インポートフックは conftest.py ファイルと、pytest11 エントリーポイントにプラグインとしてリストされているモジュールのみを扱います。 例として、次のパッケージを考えてみましょう:

pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py

次の典型的な setup.py 抜粋を使用します:

setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)

この場合、pytest_foo/plugin.py のみが書き換えられます。 ヘルパーモジュールにも書き換えが必要なアサート文が含まれている場合は、インポートされる前にそのようにマークする必要があります。 これは、パッケージ内のモジュールがインポートされるときに常に最初にインポートされる __init__.py モジュール内で書き換え用にマークするのが最も簡単です。 この方法で plugin.py は通常通り helper.py をインポートできます。 pytest_foo/__init__.py の内容は次のようになります:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

テストモジュールまたは conftest ファイルでのプラグインの要求/読み込み

pytest_plugins を使用して、テストモジュールまたは conftest.py ファイルでプラグインを要求できます:

pytest_plugins = ["name1", "name2"]

テストモジュールまたは conftest プラグインが読み込まれると、指定されたプラグインも読み込まれます。 内部アプリケーションモジュールを含む任意のモジュールをプラグインとして祝福できます:

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins は再帰的に処理されるため、上記の例で myapp.testsupport.mypluginpytest_plugins を宣言している場合、その変数の内容もプラグインとして読み込まれることに注意してください。

注釈

ルート以外の conftest.py ファイルで pytest_plugins 変数を使用してプラグインを要求することは非推奨です。

これは重要です。 なぜなら、conftest.py ファイルはディレクトリごとのフック実装を行いますが、一度プラグインがインポートされると、ディレクトリツリー全体に影響を与えるからです。 混乱を避けるために、テストのルートディレクトリにない conftest.py ファイルで pytest_plugins を定義することは非推奨であり、警告が発生します。

このメカニズムにより、entry point packaging metadata 技術を使用して外部プラグインを作成する必要なく、アプリケーション内または外部アプリケーション内でフィクスチャを簡単に共有できます。

pytest_plugins によってインポートされたプラグインも、自動的にアサーション書き換えの対象としてマークされます (pytest.register_assert_rewrite() を参照) 。 ただし、これが効果を発揮するためには、モジュールがすでにインポートされていない必要があります。 pytest_plugins ステートメントが処理された時点でモジュールがすでにインポートされていた場合、警告が発生し、プラグイン内のアサーションは書き換えられません。 これを修正するには、モジュールがインポートされる前に自分で pytest.register_assert_rewrite() を呼び出すか、プラグインが登録されるまでインポートを遅らせるようにコードを調整することができます。

名前で他のプラグインにアクセスする

プラグインが他のプラグインのコードと協力したい場合、次のようにプラグインマネージャーを通じて参照を取得できます:

plugin = config.pluginmanager.get_plugin("name_of_plugin")

既存のプラグインの名前を確認したい場合は、--trace-config オプションを使用してください。

カスタムマーカーの登録

プラグインがマーカーを使用する場合、それらを登録して pytest のヘルプテキストに表示され、cause spurious warnings を引き起こさないようにする必要があります。 たとえば、次のプラグインはすべてのユーザーに対して cool_markermark_with を登録します:

def pytest_configure(config):
    config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
    config.addinivalue_line(
        "markers", "mark_with(arg, arg2): this marker takes arguments."
    )

プラグインのテスト

pytest には pytester という名前のプラグインが付属しており、プラグインコードのテストを書くのに役立ちます。 このプラグインはデフォルトで無効になっているため、使用する前に有効にする必要があります。

テストディレクトリの conftest.py ファイルに次の行を追加することでこれを行うことができます:

# content of conftest.py

pytest_plugins = ["pytester"]

あるいは、-p pytester コマンドラインオプションを使用して pytest を呼び出すこともできます。

これにより、プラグインコードをテストするために pytester フィクスチャを使用できるようになります。

プラグインで何ができるかを例で示しましょう。 フィクスチャ hello を提供するプラグインを開発したと想像してください。 このフィクスチャは関数を生成し、この関数を1つのオプションパラメータで呼び出すことができます。 値を指定しない場合は Hello World! という文字列値を返し、値を指定した場合は Hello {value}! という文字列値を返します。

import pytest


def pytest_addoption(parser):
    group = parser.getgroup("helloworld")
    group.addoption(
        "--name",
        action="store",
        dest="name",
        default="World",
        help='Default "name" for hello().',
    )


@pytest.fixture
def hello(request):
    name = request.config.getoption("name")

    def _hello(name=None):
        if not name:
            name = request.config.getoption("name")
        return f"Hello {name}!"

    return _hello

現在、pytester フィクスチャは、一時的な conftest.py ファイルおよびテストファイルを作成するための便利な API を提供しています。 また、テストを実行して結果オブジェクトを返すこともでき、その結果を使用してテストの結果をアサートできます。

def test_hello(pytester):
    """Make sure that our plugin works."""

    # create a temporary conftest.py file
    pytester.makeconftest(
        """
        import pytest

        @pytest.fixture(params=[
            "Brianna",
            "Andreas",
            "Floris",
        ])
        def name(request):
            return request.param
    """
    )

    # create a temporary pytest test file
    pytester.makepyfile(
        """
        def test_hello_default(hello):
            assert hello() == "Hello World!"

        def test_hello_name(hello, name):
            assert hello(name) == "Hello {0}!".format(name)
    """
    )

    # run all tests with pytest
    result = pytester.runpytest()

    # check that all 4 tests passed
    result.assert_outcomes(passed=4)

さらに、pytest を実行する前に例を pytester の分離された環境にコピーすることも可能です。 この方法で、テストされるロジックを別々のファイルに抽象化することができ、特に長いテストや長い conftest.py ファイルに役立ちます。

pytester.copy_example を機能させるには、pytest.inipytester_example_dir を設定して、pytest に例ファイルの場所を知らせる必要があることに注意してください。

# content of pytest.ini
[pytest]
pytester_example_dir = .
# content of test_example.py


def test_plugin(pytester):
    pytester.copy_example("test_example.py")
    pytester.runpytest("-k", "test_example")


def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

============================ 2 passed in 0.12s =============================

runpytest() が返す結果オブジェクトおよびそれが提供するメソッドの詳細については、RunResult ドキュメントを参照してください。