pytestのおさらい

はじめに

FASTAPIでAPIを実装するんですが、久々にpythonのコードを書くので、pytestの使い方をまとめます。

pytest の基本

Python のテストフレームワークです。test_ で始まる関数を自動で見つけて実行します。

最もシンプルなテスト

# test_sample.py

def test_add():
    assert 1 + 1 == 2

def test_subtract():
    assert 5 - 3 == 2

実行

pytest test_sample.py

pytest のルール

ルール説明
ファイル名test_*.py または *_test.py
関数名test_ で始まる
クラス名Test で始まる(任意)
検証assert 文を使う

クラスでまとめる

関連するテストをクラスでグループ化できます。

# test_calculator.py

class TestCalculator:
    def test_add(self):
        assert 1 + 1 == 2
    
    def test_multiply(self):
        assert 2 * 3 == 6

テストの実行方法

# 全テスト実行
pytest

# 特定ファイル
pytest test_sample.py

# 特定クラス
pytest test_sample.py::TestCalculator

# 特定テスト
pytest test_sample.py::TestCalculator::test_add

# 詳細表示
pytest -v

pytest実践ガイド

一般的なフォルダ構成

project_root/
├── src/                    # アプリケーションコード
│   └── main.py
│
├── tests/                  # テストコード置き場
│   ├── conftest.py         # 全テスト共通の設定を記述
│   │
│   ├── fixtures/           # fixture が多い場合、ここに分割して配置
│   │   ├── __init__.py
│   │   ├── database.py
│   │   └── auth.py
│   │
│   ├── unit/               # 単体テスト (DB接続なし、モック多用)
│   │   ├── conftest.py     # unit 専用の設定があれば配置
│   │   └── test_services.py
│   │
│   └── integration/        # 統合テスト (DB接続あり、API結合など)
│       └── test_api.py
│
└── pytest.ini              # pytest 全体の設定ファイル

conftest.py の役割

conftest.pyには、「テストの準備部屋」 です。どのテストファイルからも import 無しで参照できる設定や fixture を置きます。

  • 全テストで共有したい fixture や hook (実行前後の処理) を定義する。
  • プロジェクトルートの conftest.py は全テストに適用。サブディレクトリ (conftest.py) に置くと、その階層以下のみに適用されます。
  • fixture が増えてきたら fixtures/ フォルダに分割し、conftest.py で読み込むのが定石です。

pytest_plugins は 外部の fixture ファイルを読み込むための特別な変数です。

また、pytest_configure(), pytest_unconfigure()でテストの実行前後で処理を追加できます。

# tests/conftest.py の例
pytest_plugins = [
    "fixtures.database",  # fixtures/database.py を読み込む
    "fixtures.auth",      # fixtures/auth.py を読み込む
]

def pytest_configure(config):
    """Configure pytest markers"""
    config.addinivalue_line("markers", "unit: Unit tests")
    config.addinivalue_line("markers", "integration: Integration tests")

カスタムマーカー

テストにカスタムマークを設定する

@pytest.mark.unit
def test_something():
    ...

@pytest.mark.integration
def test_api():
    ...

実行時にマーカーでフィルタリングする

pytest -m unit          # unit マーカーのみ
pytest -m integration   # integration マーカーのみ

Fixtureの詳細

基本的な書き方

import pytest

# 定義 (conftest.py やテストファイル内)
@pytest.fixture
def sample_user():
    return {"name": "Test User", "age": 30}

# 利用 (テスト関数)
# 引数名と fixture 名を一致させるだけで自動注入される
def test_user_age(sample_user):
    assert sample_user["age"] == 30

Scope

Scopeを指定して、fixture が生成・破棄されるタイミングを制御することができます。

Scope説明用途例
function(デフォルト) テスト関数ごとに作成・破棄DBセッション、テストデータ
classテストクラスごとに1回作成関連するテスト群の共通設定
moduleファイル (.py) ごとに1回作成APIクライアントの初期化
sessionテスト実行全体で1回だけ作成DBエンジンの作成、Docker起動

yield (後処理 / Teardown)

return の代わりに yield を使うと、テスト終了後に後処理を実行できます。

@pytest.fixture
def resource():
    print("セットアップ: リソース確保")
    yield "データ"
    print("ティアダウン: リソース解放")  # テスト終了後に実行される

SQLite (インメモリ) の指定

テスト実行時のみ SQLite のインメモリ DB を使うことで、高速かつクリーンな環境でテストできます。
SQLAlchemy を使った一般的な実装例です。

tests/fixtures/database.py の例:

import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from src.core.database import Base, get_db
from src.main import app

# 1. Engine は 'session' スコープ (全テストで1回だけ作成)
@pytest.fixture(scope="session")
def db_engine():
    # SQLite インメモリを指定
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False}, # SQLite用のおまじない
        poolclass=StaticPool # インメモリDBを維持するために必要
    )
    
    # テーブル作成
    Base.metadata.create_all(bind=engine)
    
    yield engine
    
    # 全テスト終了後にテーブル削除
    Base.metadata.drop_all(bind=engine)

# 2. Session は 'function' スコープ (テスト関数ごとにロールバック)
@pytest.fixture(scope="function")
def db_session(db_engine):
    connection = db_engine.connect()
    transaction = connection.begin()
    
    SessionLocal = sessionmaker(bind=connection)
    session = SessionLocal()
    
    yield session  # テスト実行
    
    session.close()
    transaction.rollback()  # DBへの変更をなかったことにする
    connection.close()

@patchによるMockの利用

from unittest.mock import patch, Mock

# 外部 API を呼ぶ関数
def get_user_from_api(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()

# テスト
@patch("requests.get")
def test_get_user(mock_get):
    # requests.getのモックがmock_getになる
    # モックの戻り値を設定
    mock_get.return_value.json.return_value = {"id": 1, "name": "太郎"}
    
    # テスト実行
    user = get_user_from_api(1)
    
    # 検証
    assert user["name"] == "太郎"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

関連記事

カテゴリー

アーカイブ

Lang »