pytest 8.x 実践入門:インストールから基本テストまで

pytest 概要

pytest は、Python 向けのテストフレームワークであり、小規模なユニットテストから複雑な機能テストまでを効率的に構築できます。样板コードを最小限に抑え、読みやすく表現力豊かなテスト記述を可能にします。

環境のセットアップ

pytest を利用するには、Python 3.8 以上または PyPy3 が必要です。以下のコマンドで最新の pytest をインストールできます。

pip install -U pytest

インストールが完了したら、バージョンを確認して正しく導入されたか検証します。

$ pytest --version
pytest 8.2.2

最初のテストケース作成

test_calculation.py というファイルを作成し、簡単な計算関数とそのテスト関数を定義します。

# content of test_calculation.py
def compute_score(base):
    return base + 10


def test_verify_score():
    assert compute_score(5) == 20

この状態で pytest を実行すると、予期せぬ失敗報告が表示されます。

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

test_calculation.py F                                                     [100%]

================================= FAILURES =================================
_____________________________ test_verify_score _____________________________

    def test_verify_score():
>       assert compute_score(5) == 20
E       assert 15 == 20
E        +  where 15 = compute_score(5)

test_calculation.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_calculation.py::test_verify_score - assert 15 == 20
============================ 1 failed in 0.10s =============================

出力結果の [100%] はテスト実行の進捗を示します。失敗報告では、compute_score(5) が 20 ではなく 15 を返したことが明確に表示されます。pytest の断言機能は、失敗した式の中間値を自動的に報告するため、デバッグが容易になります。

テストの自動検出

pytest は、現在のディレクトリおよびサブディレクトリ内にある test_*.py または *_test.py という命名規則に従うファイルを自動的に検出します。これにより、複数のテストファイルを一度に実行することが可能です。

例外発生 の検証

特定のコードブロックが例外を発生させることを検証するには、pytest.raises ヘルパーを使用します。

# content of test_error_handling.py
import pytest


def validate_input(value):
    if value < 0:
        raise ValueError("Negative value not allowed")


def test_invalid_input():
    with pytest.raises(ValueError):
        validate_input(-1)

また、Python 3.11 以降で導入された ExceptionGroup に対しても対応可能です。

# content of test_grouped_errors.py
import pytest


def trigger_errors():
    raise ExceptionGroup(
        "Multiple errors occurred",
        [
            RuntimeError("System failure"),
        ],
    )


def test_grouped_exceptions():
    with pytest.raises(ExceptionGroup) as error_info:
        trigger_errors()
    assert error_info.group_contains(RuntimeError)
    assert not error_info.group_contains(TypeError)

trigger_errors 関数は複数の例外をグループとして投げます。テスト関数内では raises コンテキストマネージャを用いてこれを捕捉し、group_contains メソッドで特定の例外タイプが含まれているかを確認します。

テストクラスによる整理

テスト数が増えた場合、クラス内にメソッドとしてまとめることで整理できます。pytest は Test で始まるクラス内の、test_ で始まるメソッドをテストとして認識します。

# content of test_validation.py
class TestValidation:
    def check_format(self):
        text = "sample"
        assert "m" in text

    def check_method(self):
        text = "hello"
        assert hasattr(text, "strip")

実行結果は以下のようになります。

$ pytest -q test_validation.py
.F                                                                   [100%]
================================= FAILURES =================================
__________________________ TestValidation.check_method ___________________________

self = <test_validation.TestValidation object at 0xdeadbeef0001>

    def check_method(self):
        text = "hello"
>       assert hasattr(text, "strip")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'strip')

test_validation.py:8: AssertionError
========================= short test summary info ==========================
FAILED test_validation.py::TestValidation::check_method - AssertionError: assert False
1 failed, 1 passed in 0.11s

クラスを用いる利点には、テストの構造化、フィクスチャの共有、クラスレベルでのマーク適用などがあります。ただし、クラスインスタンスはテストごとに新しく生成される点に注意が必要です。

# content of test_state_isolation.py
class TestSharedState:
    count = 100

    def test_increment(self):
        self.count = 101
        assert self.count == 101

    def test_check_value(self):
        assert self.count == 101
$ pytest -k TestSharedState -q
.F                                                                   [100%]
================================= FAILURES =================================
______________________ TestSharedState.test_check_value ______________________

self = <test_state_isolation.TestSharedState object at 0xdeadbeef0002>

    def test_check_value(self):
>       assert self.count == 101
E       assert 100 == 101
E        +  where 100 = <test_state_isolation.TestSharedState object at 0xdeadbeef0002>.count

test_state_isolation.py:9: AssertionError
========================= short test summary info ==========================
FAILED test_state_isolation.py::TestSharedState::test_check_value - assert 100 == 101
1 failed, 1 passed in 0.10s

クラス変数は共有されますが、インスタンス変数はテスト間でリセットされるため、テスト間の状態依存は避けるべきです。-k オプションを使用すると、特定のテストパターンにマッチするケースのみを実行できます。

一時ディレクトリの利用

ファイル操作を含むテストでは、tmp_path フィクスチャを利用することで、テストごとに孤立した一時ディレクトリを自動的に用意できます。

# content of test_workspace.py
def test_temporary_workspace(tmp_path):
    print(tmp_path)
    assert 0

テスト関数の引数に tmp_path を指定すると、pytest は実行前に独自のディレクトリを生成し、パスオブジェクトを渡します。

$ pytest -q test_workspace.py
F                                                                    [100%]
================================= FAILURES =================================
__________________________ test_temporary_workspace __________________________

tmp_path = PosixPath('PYTEST_TMPDIR/test_temporary_workspace0')

    def test_temporary_workspace(tmp_path):
        print(tmp_path)
>       assert 0
E       assert 0

test_workspace.py:3: AssertionError
--------------------------- Captured stdout call ---------------------------
PYTEST_TMPDIR/test_temporary_workspace0
========================= short test summary info ==========================
FAILED test_workspace.py::test_temporary_workspace - assert 0
1 failed in 0.10s

この仕組みにより、テスト実行後のクリーンアップが自動で行われ、並列実行時の競合も防止されます。

利用可能なフィクスチャの確認

プロジェクトで利用可能な組み込みおよびカスタムフィクスチャの一覧を表示するには、以下のコマンドを実行します。

pytest --fixtures

デフォルトではプライベート扱いとなる _ 始まりのフィクスチャは省略されます。すべてを表示するには -v オプションを併用します。

タグ: pytest Python unit-testing test-automation pytest-fixtures

7月19日 21:02 投稿