基本的なパターンと例

コマンドラインオプションのデフォルトを変更する方法

pytest を使用するたびに同じ一連のコマンドラインオプションを入力するのは面倒です。 たとえば、スキップされたテストや xfailed テストの詳細情報を常に表示し、簡潔な「ドット」進行出力を表示したい場合は、設定ファイルに書き込むことができます。

# content of pytest.ini
[pytest]
addopts = -ra -q

または、環境が使用中にコマンドラインオプションを追加するために PYTEST_ADDOPTS 環境変数を設定することもできます。

export PYTEST_ADDOPTS="-v"

addopts または環境変数が存在する場合のコマンドラインの構築方法は次のとおりです。

<pytest.ini:addopts> $PYTEST_ADDOPTS <extra command-line arguments>

したがって、ユーザーがコマンドラインで実行すると:

pytest -m slow

実際に実行されるコマンドラインは次のとおりです:

pytest -ra -q -v -m slow

他のコマンドラインアプリケーションと同様に、オプションが競合する場合は最後のものが優先されるため、上記の例では -v-q を上書きするため、詳細な出力が表示されます。

コマンドラインオプションに応じて異なる値をテスト関数に渡す

コマンドラインオプションに依存するテストを書きたいとします。 これを実現するための基本的なパターンは次のとおりです。

# content of test_sample.py
def test_answer(cmdopt):
    if cmdopt == "type1":
        print("first")
    elif cmdopt == "type2":
        print("second")
    assert 0  # to see what was printed

これを機能させるには、コマンドラインオプションを追加し、fixture function を通じて cmdopt を提供する必要があります。

# content of conftest.py
import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt", action="store", default="type1", help="my option: type1 or type2"
    )


@pytest.fixture
def cmdopt(request):
    return request.config.getoption("--cmdopt")

新しいオプションを指定せずにこれを実行してみましょう:

$ pytest -q test_sample.py
F                                                                    [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________

cmdopt = 'type1'

    def test_answer(cmdopt):
        if cmdopt == "type1":
            print("first")
        elif cmdopt == "type2":
            print("second")
>       assert 0  # to see what was printed
E       assert 0

test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
first
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 0
1 failed in 0.12s

次に、コマンドラインオプションを指定して実行します:

$ pytest -q --cmdopt=type2
F                                                                    [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________

cmdopt = 'type2'

    def test_answer(cmdopt):
        if cmdopt == "type1":
            print("first")
        elif cmdopt == "type2":
            print("second")
>       assert 0  # to see what was printed
E       assert 0

test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
second
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 0
1 failed in 0.12s

コマンドラインオプションがテストに到達したことがわかります。

選択肢をリストアップすることで、入力の簡単な検証を追加できます:

# content of conftest.py
import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt",
        action="store",
        default="type1",
        help="my option: type1 or type2",
        choices=("type1", "type2"),
    )

これで、不正な引数に対するフィードバックが得られます:

$ pytest -q --cmdopt=type3
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: argument --cmdopt: invalid choice: 'type3' (choose from 'type1', 'type2')

より詳細なエラーメッセージを提供する必要がある場合は、type パラメータを使用して pytest.UsageError を発生させることができます。

# content of conftest.py
import pytest


def type_checker(value):
    msg = "cmdopt must specify a numeric type as typeNNN"
    if not value.startswith("type"):
        raise pytest.UsageError(msg)
    try:
        int(value[4:])
    except ValueError:
        raise pytest.UsageError(msg)

    return value


def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt",
        action="store",
        default="type1",
        help="my option: type1 or type2",
        type=type_checker,
    )

これで基本パターンは完了です。 ただし、多くの場合、テストの外でコマンドラインオプションを処理し、異なるまたはより複雑なオブジェクトを渡したいと考えることがよくあります。

コマンドラインオプションを動的に追加する

addopts を通じて、プロジェクトのコマンドラインオプションを静的に追加できます。 また、処理される前にコマンドライン引数を動的に変更することもできます。

# installable external plugin
import sys


def pytest_load_initial_conftests(args):
    if "xdist" in sys.modules:  # pytest-xdist plugin
        import multiprocessing

        num = max(multiprocessing.cpu_count() / 2, 1)
        args[:] = ["-n", str(num)] + args

xdist plugin がインストールされている場合、CPU に近い数のサブプロセスを使用してテストを実行します。 上記の conftest.py を使用して空のディレクトリで実行します:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 0 items

========================== no tests ran in 0.12s ===========================

コマンドラインオプションに従ってテストのスキップを制御する

ここに、pytest.mark.slow マークされたテストのスキップを制御するための --runslow コマンドラインオプションを追加する conftest.py ファイルがあります:

# content of conftest.py

import pytest


def pytest_addoption(parser):
    parser.addoption(
        "--runslow", action="store_true", default=False, help="run slow tests"
    )


def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow to run")


def pytest_collection_modifyitems(config, items):
    if config.getoption("--runslow"):
        # --runslow given in cli: do not skip slow tests
        return
    skip_slow = pytest.mark.skip(reason="need --runslow option to run")
    for item in items:
        if "slow" in item.keywords:
            item.add_marker(skip_slow)

次のようにテストモジュールを書くことができます:

# content of test_module.py
import pytest


def test_func_fast():
    pass


@pytest.mark.slow
def test_func_slow():
    pass

実行すると、スキップされた「遅い」テストが表示されます:

$ pytest -rs    # "-rs" means report details on the little 's'
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_module.py .s                                                    [100%]

========================= short test summary info ==========================
SKIPPED [1] test_module.py:8: need --runslow option to run
======================= 1 passed, 1 skipped in 0.12s =======================

または、slow マークされたテストを含めて実行します:

$ pytest --runslow
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_module.py ..                                                    [100%]

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

統合されたアサーションヘルパーの作成

テストから呼び出されるテストヘルパー関数がある場合、pytest.fail マーカーを使用して特定のメッセージでテストを失敗させることができます。 ヘルパー関数のどこかに __tracebackhide__ オプションを設定すると、テストサポート関数はトレースバックに表示されません。 例:

# content of test_checkconfig.py
import pytest


def checkconfig(x):
    __tracebackhide__ = True
    if not hasattr(x, "config"):
        pytest.fail(f"not configured: {x}")


def test_something():
    checkconfig(42)

__tracebackhide__ 設定は、pytest のトレースバック表示に影響します: --full-trace コマンドラインオプションが指定されない限り、checkconfig 関数は表示されません。 小さな関数を実行してみましょう:

$ pytest -q test_checkconfig.py
F                                                                    [100%]
================================= FAILURES =================================
______________________________ test_something ______________________________

    def test_something():
>       checkconfig(42)
E       Failed: not configured: 42

test_checkconfig.py:11: Failed
========================= short test summary info ==========================
FAILED test_checkconfig.py::test_something - Failed: not configured: 42
1 failed in 0.12s

特定の例外のみを非表示にしたい場合は、__tracebackhide__ExceptionInfo オブジェクトを取得する呼び出し可能なものに設定できます。 たとえば、予期しない例外タイプが非表示にならないようにするためにこれを使用できます:

import operator

import pytest


class ConfigException(Exception):
    pass


def checkconfig(x):
    __tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
    if not hasattr(x, "config"):
        raise ConfigException(f"not configured: {x}")


def test_something():
    checkconfig(42)

これにより、無関係な例外 (つまり、アサーションヘルパーのバグ) で例外トレースバックが非表示になるのを防ぎます。

pytest 実行内から実行されているかどうかを検出する

通常、テストから呼び出された場合にアプリケーションコードの動作を変更するのは良くない考えです。 ただし、アプリケーションコードがテストから実行されているかどうかを絶対に確認する必要がある場合は、次のようにします:

import os


if os.environ.get("PYTEST_VERSION") is not None:
    # Things you want to to do if your code is called by pytest.
    ...
else:
    # Things you want to to do if your code is not called by pytest.
    ...

テストレポートヘッダーに情報を追加する

pytest 実行で追加情報を表示するのは簡単です:

# content of conftest.py


def pytest_report_header(config):
    return "project deps: mylib-1.1"

これにより、文字列がテストヘッダーに適宜追加されます:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
project deps: mylib-1.1
rootdir: /home/sweet/project
collected 0 items

========================== no tests ran in 0.12s ===========================

複数行の情報として扱われる文字列のリストを返すことも可能です。 適用可能な場合は、config.getoption('verbose') を考慮して、より多くの情報を表示することができます:

# content of conftest.py


def pytest_report_header(config):
    if config.get_verbosity() > 0:
        return ["info1: did you know that ...", "did you?"]

これにより、"--v" で実行した場合にのみ情報が追加されます:

$ pytest -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
info1: did you know that ...
did you?
rootdir: /home/sweet/project
collecting ... collected 0 items

========================== no tests ran in 0.12s ===========================

通常の実行では何も表示されません:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 0 items

========================== no tests ran in 0.12s ===========================

テストの実行時間をプロファイリングする

実行が遅い大規模なテストスイートがある場合、どのテストが最も遅いかを確認したいかもしれません。 人工的なテストスイートを作成してみましょう:

# content of test_some_are_slow.py
import time


def test_funcfast():
    time.sleep(0.1)


def test_funcslow1():
    time.sleep(0.2)


def test_funcslow2():
    time.sleep(0.3)

これで、どのテスト関数が最も遅く実行されるかをプロファイリングできます:

$ pytest --durations=3
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_some_are_slow.py ...                                            [100%]

=========================== slowest 3 durations ============================
0.30s call     test_some_are_slow.py::test_funcslow2
0.20s call     test_some_are_slow.py::test_funcslow1
0.10s call     test_some_are_slow.py::test_funcfast
============================ 3 passed in 0.12s =============================

インクリメンタルテスト - テストステップ

一連のテストステップで構成されるテスト状況があるかもしれません。 1つのステップが失敗すると、他のステップもすべて失敗することが予想されるため、さらにステップを実行する意味はありませんし、そのトレースバックは何の洞察も与えません。 ここに、クラスで使用する incremental マーカーを導入するシンプルな conftest.py ファイルがあります:

# content of conftest.py

from typing import Dict, Tuple

import pytest

# store history of failures per test class name and per index in parametrize (if parametrize used)
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}


def pytest_runtest_makereport(item, call):
    if "incremental" in item.keywords:
        # incremental marker is used
        if call.excinfo is not None:
            # the test has failed
            # retrieve the class name of the test
            cls_name = str(item.cls)
            # retrieve the index of the test (if parametrize is used in combination with incremental)
            parametrize_index = (
                tuple(item.callspec.indices.values())
                if hasattr(item, "callspec")
                else ()
            )
            # retrieve the name of the test function
            test_name = item.originalname or item.name
            # store in _test_failed_incremental the original name of the failed test
            _test_failed_incremental.setdefault(cls_name, {}).setdefault(
                parametrize_index, test_name
            )


def pytest_runtest_setup(item):
    if "incremental" in item.keywords:
        # retrieve the class name of the test
        cls_name = str(item.cls)
        # check if a previous test has failed for this class
        if cls_name in _test_failed_incremental:
            # retrieve the index of the test (if parametrize is used in combination with incremental)
            parametrize_index = (
                tuple(item.callspec.indices.values())
                if hasattr(item, "callspec")
                else ()
            )
            # retrieve the name of the first test function to fail for this class name and index
            test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
            # if name found, test has failed for the combination of class name & test name
            if test_name is not None:
                pytest.xfail(f"previous test failed ({test_name})")

これらの2つのフック実装は連携して、クラス内のインクリメンタルマークされたテストを中止します。 ここにテストモジュールの例があります:

# content of test_step.py

import pytest


@pytest.mark.incremental
class TestUserHandling:
    def test_login(self):
        pass

    def test_modification(self):
        assert 0

    def test_deletion(self):
        pass


def test_normal():
    pass

これを実行すると:

$ pytest -rx
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items

test_step.py .Fx.                                                    [100%]

================================= FAILURES =================================
____________________ TestUserHandling.test_modification ____________________

self = <test_step.TestUserHandling object at 0xdeadbeef0001>

    def test_modification(self):
>       assert 0
E       assert 0

test_step.py:11: AssertionError
========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================

test_modification が失敗したため、test_deletion が実行されなかったことがわかります。 これは「予想される失敗」として報告されます。

パッケージ/ディレクトリレベルのフィクスチャ (セットアップ)

ネストされたテストディレクトリがある場合、そのディレクトリに conftest.py ファイルを配置することで、ディレクトリごとのフィクスチャスコープを持つことができます。 xUnit のセットアップ/ティアダウンの概念に相当する autouse fixtures を含むすべてのタイプのフィクスチャを使用できます。 ただし、特に実際のテストから遠く離れている場合は、暗黙的にセットアップ/ティアダウン関数を実行するのではなく、テストやテストクラスで明示的なフィクスチャ参照を持つことをお勧めします。

ディレクトリで db フィクスチャを利用可能にする例は次のとおりです:

# content of a/conftest.py
import pytest


class DB:
    pass


@pytest.fixture(scope="package")
def db():
    return DB()

次に、そのディレクトリ内のテストモジュール:

# content of a/test_db.py
def test_a1(db):
    assert 0, db  # to show value

別のテストモジュール:

# content of a/test_db2.py
def test_a2(db):
    assert 0, db  # to show value

次に、db フィクスチャが見えない姉妹ディレクトリ内のモジュール:

# content of b/test_error.py
def test_root(db):  # no db here, will error out
    pass

これを実行できます:

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 7 items

a/test_db.py F                                                       [ 14%]
a/test_db2.py F                                                      [ 28%]
b/test_error.py E                                                    [ 42%]
test_step.py .Fx.                                                    [100%]

================================== ERRORS ==================================
_______________________ ERROR at setup of test_root ________________________
file /home/sweet/project/b/test_error.py, line 1
  def test_root(db):  # no db here, will error out
E       fixture 'db' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

/home/sweet/project/b/test_error.py:1
================================= FAILURES =================================
_________________________________ test_a1 __________________________________

db = <conftest.DB object at 0xdeadbeef0002>

    def test_a1(db):
>       assert 0, db  # to show value
E       AssertionError: <conftest.DB object at 0xdeadbeef0002>
E       assert 0

a/test_db.py:2: AssertionError
_________________________________ test_a2 __________________________________

db = <conftest.DB object at 0xdeadbeef0002>

    def test_a2(db):
>       assert 0, db  # to show value
E       AssertionError: <conftest.DB object at 0xdeadbeef0002>
E       assert 0

a/test_db2.py:2: AssertionError
____________________ TestUserHandling.test_modification ____________________

self = <test_step.TestUserHandling object at 0xdeadbeef0003>

    def test_modification(self):
>       assert 0
E       assert 0

test_step.py:11: AssertionError
========================= short test summary info ==========================
FAILED a/test_db.py::test_a1 - AssertionError: <conftest.DB object at 0x7...
FAILED a/test_db2.py::test_a2 - AssertionError: <conftest.DB object at 0x...
FAILED test_step.py::TestUserHandling::test_modification - assert 0
ERROR b/test_error.py::test_root
============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ==============

a ディレクトリ内の2つのテストモジュールは同じ db フィクスチャインスタンスを参照しますが、姉妹ディレクトリ b 内の1つのテストはそれを参照しません。 もちろん、姉妹ディレクトリの conftest.py ファイルに db フィクスチャを定義することもできます。 各フィクスチャは、実際に必要なテストがある場合にのみインスタンス化されることに注意してください (「autouse」フィクスチャを使用しない限り、最初のテスト実行の前に常に実行されます) 。

テストレポート/失敗の後処理

テストレポートを後処理したい場合や、実行環境にアクセスする必要がある場合は、テスト「レポート」オブジェクトが作成される直前に呼び出されるフックを実装できます。 ここでは、すべての失敗したテスト呼び出しを書き出し、テストで使用された場合にはフィクスチャにもアクセスします。 後処理中にそれを照会/確認したい場合に備えて、情報を「failures」ファイルに書き出します。

# content of conftest.py

import os.path

import pytest


@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    # execute all other hooks to obtain the report object
    rep = yield

    # we only look at actual failing test calls, not setup/teardown
    if rep.when == "call" and rep.failed:
        mode = "a" if os.path.exists("failures") else "w"
        with open("failures", mode, encoding="utf-8") as f:
            # let's also access a fixture for the fun of it
            if "tmp_path" in item.fixturenames:
                extra = " ({})".format(item.funcargs["tmp_path"])
            else:
                extra = ""

            f.write(rep.nodeid + extra + "\n")

    return rep

その後、失敗したテストがある場合:

# content of test_module.py
def test_fail1(tmp_path):
    assert 0


def test_fail2():
    assert 0

それらを実行します:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_module.py FF                                                    [100%]

================================= FAILURES =================================
________________________________ test_fail1 ________________________________

tmp_path = PosixPath('PYTEST_TMPDIR/test_fail10')

    def test_fail1(tmp_path):
>       assert 0
E       assert 0

test_module.py:2: AssertionError
________________________________ test_fail2 ________________________________

    def test_fail2():
>       assert 0
E       assert 0

test_module.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_fail1 - assert 0
FAILED test_module.py::test_fail2 - assert 0
============================ 2 failed in 0.12s =============================

失敗したテストIDを含む「failures」ファイルが作成されます:

$ cat failures
test_module.py::test_fail1 (PYTEST_TMPDIR/test_fail10)
test_module.py::test_fail2

フィクスチャでテスト結果情報を利用可能にする

フィクスチャのファイナライザでテスト結果レポートを利用可能にしたい場合は、ローカルプラグインを介して実装された小さな例を示します:

# content of conftest.py
from typing import Dict
import pytest
from pytest import StashKey, CollectReport

phase_report_key = StashKey[Dict[str, CollectReport]]()


@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    # execute all other hooks to obtain the report object
    rep = yield

    # store test results for each phase of a call, which can
    # be "setup", "call", "teardown"
    item.stash.setdefault(phase_report_key, {})[rep.when] = rep

    return rep


@pytest.fixture
def something(request):
    yield
    # request.node is an "item" because we use the default
    # "function" scope
    report = request.node.stash[phase_report_key]
    if report["setup"].failed:
        print("setting up a test failed or skipped", request.node.nodeid)
    elif ("call" not in report) or report["call"].failed:
        print("executing test failed or skipped", request.node.nodeid)

その後、失敗したテストがある場合:

# content of test_module.py

import pytest


@pytest.fixture
def other():
    assert 0


def test_setup_fails(something, other):
    pass


def test_call_fails(something):
    assert 0


def test_fail2():
    assert 0

それを実行します:

$ pytest -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 3 items

test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed or skipped test_module.py::test_call_fails
F

================================== ERRORS ==================================
____________________ ERROR at setup of test_setup_fails ____________________

    @pytest.fixture
    def other():
>       assert 0
E       assert 0

test_module.py:7: AssertionError
================================= FAILURES =================================
_____________________________ test_call_fails ______________________________

something = None

    def test_call_fails(something):
>       assert 0
E       assert 0

test_module.py:15: AssertionError
________________________________ test_fail2 ________________________________

    def test_fail2():
>       assert 0
E       assert 0

test_module.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_call_fails - assert 0
FAILED test_module.py::test_fail2 - assert 0
ERROR test_module.py::test_setup_fails - assert 0
======================== 2 failed, 1 error in 0.12s ========================

フィクスチャのファイナライザが正確なレポート情報を使用できることがわかります。

PYTEST_CURRENT_TEST 環境変数

テストセッションがスタックすることがあり、どのテストがスタックしたのかを簡単に把握する方法がない場合があります。 例えば、pytestが静かなモード (-q) で実行された場合や、コンソール出力にアクセスできない場合です。 これは特に、問題が断続的にしか発生しない場合、有名な「不安定な」テストの種類で問題になります。

pytest はテストを実行する際に PYTEST_CURRENT_TEST 環境変数を設定します。 これにより、必要に応じてプロセス監視ユーティリティや psutil のようなライブラリを使用して、どのテストがスタックしたかを確認できます:

import psutil

for pid in psutil.pids():
    environ = psutil.Process(pid).environ()
    if "PYTEST_CURRENT_TEST" in environ:
        print(f'pytest process {pid} running: {environ["PYTEST_CURRENT_TEST"]}')

テストセッション中、pytestは PYTEST_CURRENT_TEST を現在のテスト nodeid と現在のステージ (setupcall、または teardown) に設定します。

例えば、foo_module.py から test_foo という名前の単一のテスト関数を実行する場合、PYTEST_CURRENT_TEST は次のように設定されます:

  1. foo_module.py::test_foo (setup)

  2. foo_module.py::test_foo (call)

  3. foo_module.py::test_foo (teardown)

その順序で。

注釈

PYTEST_CURRENT_TEST の内容は人間が読める形式を意図しており、リリース間 (バグ修正を含む) で実際の形式が変更される可能性があるため、スクリプトや自動化には依存しないでください。

pytestのフリーズ

エンドユーザーに配布するために PyInstaller のようなツールを使用してアプリケーションをフリーズする場合、テストランナーもパッケージ化し、フローズンアプリケーションを使用してテストを実行することをお勧めします。 これにより、依存関係が実行可能ファイルに含まれていないなどのパッケージングエラーを早期に検出できるだけでなく、ユーザーにテストファイルを送信して自分のマシンで実行してもらうことができ、再現が難しいバグに関する情報を取得するのに役立ちます。

幸いなことに、最近の PyInstaller リリースにはすでにpytest用のカスタムフックがありますが、cx_freezepy2exe などの他のツールを使用して実行可能ファイルをフリーズする場合は、pytest.freeze_includes() を使用して内部pytestモジュールの完全なリストを取得できます。 ただし、ツールごとに内部モジュールを見つけるための設定方法は異なります。

pytestランナーを別の実行可能ファイルとしてフリーズする代わりに、プログラムの起動時に巧妙な引数処理を行うことで、フローズンプログラムをpytestランナーとして機能させることができます。 これにより、通常はより便利な単一の実行可能ファイルを持つことができます。 ただし、pytestが使用するプラグイン検出メカニズム (entry points) はフローズン実行可能ファイルでは機能しないため、pytestはサードパーティプラグインを自動的に見つけることができません。 pytest-timeout のようなサードパーティプラグインを含めるには、それらを明示的にインポートし、pytest.mainに渡す必要があります。

# contents of app_main.py
import sys

import pytest_timeout  # Third party plugin

if len(sys.argv) > 1 and sys.argv[1] == "--pytest":
    import pytest

    sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
else:
    # normal application execution: at this point argv can be parsed
    # by your argument-parsing library of choice as usual
    ...

これにより、標準の pytest コマンドラインオプションを使用してフローズンアプリケーションを使用してテストを実行できます:

./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/