AI

pytestの使い方完全ガイド【インストールから応用まで】

pytestの使い方完全ガイド【インストールから応用まで】

Python開発において、コードの品質を担保するテストは不可欠です。しかし、テストコードの記述は時に大きな負担となり、開発者の頭を悩ませます。

もしテストコードが複雑で、実行に時間がかかり、バグ修正のたびに手動で動作確認を繰り返すとしたらどうでしょうか。リリースへの不安は募り、新しい機能開発にも尻込みしてしまうかもしれません。これは、テストがない世界、あるいはテストが非効率な世界です。

一方で、簡潔なテストコードで素早くバグを見つけ出し、安心してデプロイできる世界もあります。pytestは、まさにその世界を実現するための強力なツールです。本記事では、Pythonテストフレームワークの標準であるpytestについて、インストールから応用的な使い方まで、詳しく解説します。

pytestとは

pytestは、Pythonで書かれたプログラムのテストを行うためのフレームワークです。Pythonの標準的なテストフレームワークとして、多くの開発者に利用されています。

30秒で分かる概要

pytestの最大の強みは、その簡潔な構文と豊富な機能です。最小限のコードでテストを記述でき、テストの自動検出、詳細なレポート生成、プラグインによる拡張性など、テストに必要なあらゆる機能を提供します。これにより、開発者はテストコードの記述に要する労力を減らし、本質的な開発に集中できます。

誕生した背景

Pythonには標準ライブラリとしてunittestというテストフレームワークが存在します。しかし、unittestはJavaのJUnitに影響を受けており、クラスベースの記述が必須です。このため、シンプルなテストでも多くの定型コードを書く必要がありました。

pytestは、よりPythonicで簡潔なテスト記述を求める声に応える形で誕生しました。関数ベースのテスト記述を可能にし、特別なクラスを継承する必要がありません。これにより、unittestに比べて学習コストが低く、直感的にテストを書けるようになりました。

インストール方法

pytestのインストールは非常に簡単です。Pythonのパッケージ管理ツールであるpipを使用します。

macOS / Linux / Windows 共通

まず、ターミナルまたはコマンドプロンプトを開きます。

pip install pytest

このコマンドを実行すると、pytestとその依存関係がインストールされます。

仮想環境でのインストールを推奨

プロジェクトごとに異なるPython環境やライブラリを使う場合、仮想環境の利用が推奨されます。仮想環境内でpytestをインストールすることで、プロジェクト間の依存関係の衝突を防げます。

例えば、venvを使って仮想環境を作成し、アクティベートする手順は次の通りです。

# プロジェクトディレクトリに移動
cd your_project_directory

# 仮想環境を作成
python -m venv .venv

# 仮想環境をアクティベート
# macOS / Linux の場合
source .venv/bin/activate

# Windows の場合
.venv\Scripts\activate

# 仮想環境内でpytestをインストール
pip install pytest

仮想環境をアクティベートすると、その環境内でインストールされたパッケージのみが利用可能になります。

基本的な使い方

pytestを使い始めるために、最低限知っておくべきコマンドと記述方法を解説します。

1. テストファイルの作成

pytestは、ファイル名がtest_で始まるか、_test.pyで終わるPythonファイルを自動的にテストファイルとして認識します。また、テストファイル内の関数名がtest_で始まるものをテスト関数として扱います。

例えば、test_example.pyというファイルを作成し、以下のコードを記述します。

# test_example.py

def add(a, b):
    """二つの数値を加算する関数"""
    return a + b

def test_add_positive_numbers():
    """正の数の加算をテストする"""
    assert add(1, 2) == 3

def test_add_negative_numbers():
    """負の数の加算をテストする"""
    assert add(-1, -2) == -3

def test_add_zero():
    """ゼロとの加算をテストする"""
    assert add(0, 5) == 5
    assert add(5, 0) == 5

ここでは、add関数をテストする3つのテスト関数を定義しています。assert文を使って、期待する結果が得られるかを確認します。

2. テストの実行

テストファイルを作成したら、ターミナルでpytestコマンドを実行します。

pytest

このコマンドを実行すると、pytestはカレントディレクトリとそのサブディレクトリからテストファイルを自動的に探し出し、テストを実行します。

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

============================= test session starts ==============================
platform linux -- Python 3.9.7, pytest-6.2.5, pluggy-0.13.1
rootdir: /path/to/your_project
collected 3 items

test_example.py ...                                                      [100%]

============================== 3 passed in 0.01s ===============================

...は各テストが成功したことを示します。全てのテストが成功すると、「3 passed」のようなメッセージが表示されます。

3. 詳細な結果表示 (-v)

テストの実行結果をより詳細に表示するには、-v(verbose)オプションを使います。

pytest -v

これにより、どのテスト関数が実行され、その結果が成功(PASSED)か失敗(FAILED)かが明確に表示されます。

============================= test session starts ==============================
platform linux -- Python 3.9.7, pytest-6.2.5, pluggy-0.13.1
rootdir: /path/to/your_project
collected 3 items

test_example.py::test_add_positive_numbers PASSED                        [ 33%]
test_example.py::test_add_negative_numbers PASSED                        [ 66%]
test_example.py::test_add_zero PASSED                                    [100%]

============================== 3 passed in 0.01s ===============================

4. 特定のファイルやディレクトリのテスト

プロジェクトが大きくなると、全てのテストを実行するのではなく、特定のテストだけを実行したい場合があります。ファイル名やディレクトリ名を指定することで、対象を絞れます。

# 特定のファイル内のテストを実行
pytest test_example.py

# 特定のディレクトリ内のテストを実行
pytest tests/unit/

5. 失敗時に停止 (-x)

テストが失敗した場合、通常は全てのテストを実行し続けます。しかし、最初の失敗でテストを停止させたい場合は、-xオプションを使用します。

pytest -x

これは、多数のテストがある中で、特定のバグが原因で多くのテストが失敗している場合に、原因究明を早めるのに役立ちます。

便利な使い方・応用例 3選

pytestには、テストをより効率的かつ強力にするための多くの機能があります。ここでは、実際の開発シーンで役立つ3つの応用例を紹介します。

1. パラメータ化テスト (pytest.mark.parametrize)

同じテストロジックを複数の異なる入力値で実行したい場合、pytest.mark.parametrizeデコレータが非常に便利です。これにより、重複したテストコードを書くことなく、様々なケースを網羅できます。

例えば、先ほどのadd関数に様々な入力値を与えてテストするケースを考えます。

# test_add_parametrized.py
import pytest

def add(a, b):
    return a + b

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
    (-5, -3, -8),
])
def test_add_function_with_various_inputs(a, b, expected):
    """様々な入力値でadd関数をテストする"""
    assert add(a, b) == expected

このコードでは、@pytest.mark.parametrizeデコレータを使って、テスト関数test_add_function_with_various_inputsが5つの異なる引数セットで実行されるように指定しています。a, b, expectedは引数の名前、リスト内のタプルがそれぞれの引数に渡される値です。

実行すると、pytestはこれを5つの独立したテストとして扱います。

pytest -v test_add_parametrized.py
============================= test session starts ==============================
...
test_add_parametrized.py::test_add_function_with_various_inputs[1-2-3] PASSED [ 20%]
test_add_parametrized.py::test_add_function_with_various_inputs[0-0-0] PASSED [ 40%]
test_add_parametrized.py::test_add_function_with_various_inputs[-1-1-0] PASSED [ 60%]
test_add_parametrized.py::test_add_function_with_various_inputs[100-200-300] PASSED [ 80%]
test_add_parametrized.py::test_add_function_with_various_inputs[-5--3--8] PASSED [100%]

============================== 5 passed in 0.01s ===============================

2. フィクスチャ (pytest.fixture)

テストの準備(セットアップ)や後処理(ティアダウン)は、多くのテストで共通して必要になる場合があります。例えば、データベース接続の確立、一時ファイルの作成、モックオブジェクトの準備などです。pytestのフィクスチャは、これらの共通処理をカプセル化し、テスト関数に注入する仕組みを提供します。

フィクスチャは@pytest.fixtureデコレータを使って定義します。

# test_fixture_example.py
import pytest
import os

@pytest.fixture
def setup_temp_file():
    """テスト用に一時ファイルを作成し、テスト後に削除するフィクスチャ"""
    file_path = "temp_test_file.txt"
    with open(file_path, "w") as f:
        f.write("Test data")
    print(f"\nCreated temporary file: {file_path}") # テスト実行中に表示される
    yield file_path # テスト関数にファイルパスを提供する
    os.remove(file_path)
    print(f"\nRemoved temporary file: {file_path}") # テスト後に表示される

def test_read_temp_file(setup_temp_file):
    """一時ファイルの内容を読み込むテスト"""
    path = setup_temp_file # フィクスチャが提供する値を受け取る
    with open(path, "r") as f:
        content = f.read()
    assert content == "Test data"

def test_temp_file_exists(setup_temp_file):
    """一時ファイルが存在するかをテストする"""
    path = setup_temp_file
    assert os.path.exists(path)

setup_temp_fileフィクスチャは、一時ファイルを作成し、そのパスをyieldでテスト関数に渡します。テスト関数の実行が完了すると、yield以降のコードが実行され、一時ファイルが削除されます。

テスト関数がフィクスチャを利用するには、引数としてフィクスチャ名を指定するだけです。

3. モック・スタブ化 (pytest-mock)

外部サービスへのAPI呼び出し、データベースへのアクセス、ファイルシステム操作など、テスト対象のコードが外部に依存している場合、実際の外部システムをテスト中に使用するのは非効率的です。また、テストが不安定になる原因にもなります。

このような場合、外部依存をモック(偽物)に置き換えてテストを行います。pytest-mockは、Python標準のunittest.mockライブラリをpytestで使いやすくするためのプラグインです。

まず、pytest-mockをインストールします。

pip install pytest-mock

例えば、Web APIからデータを取得する関数をテストするケースを考えます。

# my_module.py
import requests

def fetch_user_data(user_id):
    """指定されたユーザーIDのデータをAPIから取得する"""
    url = f"https://api.example.com/users/{user_id}"
    response = requests.get(url)
    response.raise_for_status() # HTTPエラーがあれば例外を発生
    return response.json()

# test_my_module.py
import my_module

def test_fetch_user_data_success(mocker):
    """ユーザーデータ取得が成功するケースをテストする"""
    # requests.get のモックオブジェクトを作成
    mock_response = mocker.Mock()
    mock_response.json.return_value = {"id": 1, "name": "Test User"}
    mock_response.raise_for_status.return_value = None # エラーなし

    # my_module.requests.get をモックオブジェクトで置き換える
    mocker.patch('my_module.requests.get', return_value=mock_response)

    # テスト対象の関数を実行
    user_data = my_module.fetch_user_data(1)

    # 結果を検証
    assert user_data == {"id": 1, "name": "Test User"}
    # requests.get が正しいURLで呼び出されたか検証
    my_module.requests.get.assert_called_once_with("https://api.example.com/users/1")

def test_fetch_user_data_failure(mocker):
    """ユーザーデータ取得が失敗するケースをテストする"""
    mock_response = mocker.Mock()
    # raise_for_status が例外を発生するように設定
    mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")

    mocker.patch('my_module.requests.get', return_value=mock_response)

    # HTTPError が発生することを検証
    with pytest.raises(requests.exceptions.HTTPError):
        my_module.fetch_user_data(999)

テスト関数はmockerというフィクスチャを受け取ります。これはpytest-mockによって提供されるもので、mocker.patchを使ってrequests.get関数を一時的に置き換えています。これにより、実際のAPIにアクセスすることなく、APIからのレスポンスをシミュレートしてテストを実行できます。

他ツールとの組み合わせ

pytestは単体でも強力ですが、他のツールと組み合わせることで、開発ワークフローをさらに強化できます。

1. CI/CDツール (GitHub Actionsなど)

継続的インテグレーション/継続的デリバリー(CI/CD)パイプラインにpytestを組み込むことは、コードの変更が加わるたびに自動的にテストを実行し、バグを早期に発見するために非常に重要です。

例えば、GitHub ActionsでPythonプロジェクトのテストを実行するワークフローの例を示します。プロジェクトの.github/workflowsディレクトリにpython-test.ymlのようなファイルを作成します。

# .github/workflows/python-test.yml
name: Python Tests

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest # テストを実行するOS環境

    steps:
    - uses: actions/checkout@v3 # リポジトリのコードをチェックアウト
    - name: Set up Python # Python環境をセットアップ
      uses: actions/setup-python@v4
      with:
        python-version: '3.x' # 使用するPythonのバージョン
    - name: Install dependencies # 依存関係をインストール
      run: |
        python -m pip install --upgrade pip
        pip install pytest # pytestをインストール
        # プロジェクトのrequirements.txtがある場合
        # pip install -r requirements.txt
    - name: Run tests with pytest # pytestを実行
      run: |
        pytest

このワークフローは、mainブランチへのプッシュやプルリクエストがあった際に自動的にトリガーされます。指定されたPythonバージョンで依存関係をインストールし、pytestコマンドを実行します。テストが成功すればワークフローは成功し、失敗すれば通知されます。

2. カバレッジツール (pytest-cov)

テストカバレッジとは、テストによってどのくらいのコードが実行されたかを示す指標です。テストカバレッジを測定することで、テストが不十分な部分を特定し、より堅牢なテストスイートを構築するのに役立ちます。

pytest-covは、pytestcoverage.pyを連携させるプラグインです。

まず、インストールします。

pip install pytest-cov

次に、pytestを実行する際に--covオプションと--cov-reportオプションを追加します。

# my_module.py のテストカバレッジを測定し、ターミナルにレポートを表示
pytest --cov=my_module --cov-report term-missing

--cov=my_moduleは、my_moduleパッケージ(またはファイル)のカバレッジを測定するようpytest-covに指示します。--cov-report term-missingは、カバレッジレポートをターミナルに表示し、テストで実行されなかった行も併せて表示します。

他にも、HTML形式のレポートを生成することも可能です。

# HTML形式でレポートを生成
pytest --cov=my_module --cov-report html

これにより、htmlcovディレクトリが作成され、ブラウザで詳細なカバレッジレポートを確認できます。

3. リンター / フォーマッター (flake8, black)

コードの品質と一貫性を保つために、リンター(構文チェック)やフォーマッター(コード整形)は不可欠です。pytest自体は直接これらと連携しませんが、CI/CDパイプライン内でテスト実行前にこれらを適用することで、高品質なコードベースを維持できます。

例えば、GitHub Actionsのワークフローにflake8blackを追加するステップは以下のようになります。

# ... (依存関係インストール後)
    - name: Lint with flake8
      run: |
        pip install flake8
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Format with Black (check only)
      run: |
        pip install black
        black --check .
    - name: Run tests with pytest
      run: |
        pytest

この例では、pytestの前にflake8でコードの品質をチェックし、black --checkでコードが正しくフォーマットされているかを確認しています。これらのステップが失敗した場合、pytestが実行される前にビルドが失敗します。

よくある設定・カスタマイズ

pytestは、pytest.iniという設定ファイルを使用することで、挙動を細かくカスタマイズできます。プロジェクトのルートディレクトリにpytest.iniファイルを作成し、以下の設定を記述します。

pytest.iniの基本構成

# pytest.ini

[pytest]
# テスト対象のディレクトリを指定
testpaths =
    tests
    src/my_app/tests

# テストファイルを検出するパターン
python_files =
    test_*.py
    *_test.py

# テスト関数を検出するパターン
python_functions =
    test_*

# テストクラスを検出するパターン (クラスベースのテストの場合)
python_classes =
    Test*

# pytestの実行時に常に適用されるオプション
addopts =
    -ra  # テスト結果の概要を表示
    --strict-markers # 未登録のマーカーの使用を禁止

# テストにマーカーを登録
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: marks tests as integration tests

各設定項目の解説

  • testpaths: pytestがテストファイルを探すディレクトリを指定します。複数のパスを指定可能です。例えば、testsディレクトリとsrc/my_app/testsディレクトリの両方からテストを探すように設定できます。
  • python_files: テストファイルとして認識するファイル名のパターンを指定します。デフォルトはtest_*.py*_test.pyです。
  • python_functions: テスト関数として認識する関数名のパターンを指定します。デフォルトはtest_*です。
  • python_classes: テストクラスとして認識するクラス名のパターンを指定します。デフォルトはTest*です。
  • addopts: pytestコマンドを実行する際に、常に適用されるオプションを指定します。
    • -ra: 失敗したテストとスキップされたテストの簡潔な概要を表示します。
    • --strict-markers: pytest.inimarkersセクションに登録されていないマーカーが使用された場合に警告を発します。これにより、スペルミスによるマーカーの誤用を防げます。

  • markers: pytest.markデコレータで使用するカスタムマーカーを登録します。登録することで、--strict-markersオプションが有効な場合でも警告が出なくなります。また、マーカーの目的を説明するコメントも記述できます。
    • 例えば、pytest -m "slow"で遅いテストのみを実行したり、pytest -m "not slow"で遅いテストを除外して実行したりできます。
  • これらの設定をpytest.iniに記述することで、プロジェクトのテスト検出ルールや実行オプションを統一し、チームメンバー間での一貫したテスト環境を構築できます。

    今日からできる実行プラン

    pytestの導入は、小さな一歩から始められます。以下の3ステップで、今日からpytestをあなたの開発ワークフローに取り入れましょう。

    ステップ1: pytestをインストールする

    まずは、あなたの開発環境にpytestを導入します。プロジェクトごとに仮想環境を作成し、その中にインストールすることをおすすめします。

    # 仮想環境を作成(例: .venv)
    python -m venv .venv
    
    # 仮想環境をアクティベート
    # macOS / Linux
    source .venv/bin/activate
    # Windows
    .venv\Scripts\activate
    
    # pytestをインストール
    pip install pytest
    

    ステップ2: 簡単なテストを書いて実行してみる

    次に、pytestの基本的な挙動を確認するために、簡単なテストファイルを作成します。

    プロジェクトのルートディレクトリにtest_sample.pyというファイルを作成し、以下のコードを記述してください。

    # test_sample.py
    
    def multiply(a, b):
        return a * b
    
    def test_multiply_positive_numbers():
        assert multiply(2, 3) == 6
    
    def test_multiply_by_zero():
        assert multiply(10, 0) == 0
    

    ファイルを保存したら、アクティベートした仮想環境でpytestコマンドを実行します。

    pytest
    

    テストが成功したことを確認できれば、pytestの基本的な使い方はマスターしたことになります。

    ステップ3: 既存プロジェクトにテストを導入する

    最後に、既存のプロジェクトにpytestを段階的に導入することを検討します。

    • 小さな機能から始める: まずは、新しく開発する機能や、既存の機能の中でも特に重要な部分からテストを書き始めます。全てのコードに一度にテストを書こうとすると、負担が大きくなります。
    • テストディレクトリを作成する: プロジェクトのルートディレクトリにtests/などのディレクトリを作成し、その中にテストファイルを整理して配置します。
    • CI/CDへの組み込みを検討する: テストが書けるようになったら、GitHub ActionsなどのCI/CDツールに自動テストのステップを追加することを検討してください。これにより、コード変更のたびにテストが自動実行され、早期に問題を発見できます。

    pytestは、簡潔な記述と強力な機能で、あなたのPython開発をより楽しく、そして安全なものに変えてくれるでしょう。ぜひ、今日からpytestを活用してみてください。

    参考文献

    広告

    -AI