はじめに
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.pypytest のルール
| ルール | 説明 |
|---|---|
| ファイル名 | 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 -vpytest実践ガイド
一般的なフォルダ構成
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"] == 30Scope
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")