pytest
テストフレームワーク
pytest
概要
pytestは、Pythonにおける最も人気の高いテストフレームワークです。シンプルな構文で始められながら、強力なフィクスチャシステム、パラメータ化、豊富なプラグインエコシステムにより、あらゆる規模のプロジェクトに対応できる柔軟性を持っています。assert文の分析機能により詳細なエラー情報を提供し、初心者からエンタープライズレベルまで幅広く採用されています。
詳細
特徴
- シンプルな構文: 関数ベースのテスト記述で学習コストが低い
- 強力なフィクスチャシステム: テストデータの準備・クリーンアップを自動化
- パラメータ化テスト: 同一ロジックを複数の入力値で効率的にテスト
- 豊富なプラグインエコシステム: 800以上のプラグインで機能拡張
- 詳細なアサーション: assert文の分析により具体的なエラー情報を表示
- 柔軟な実行制御: マーカーによるテスト分類と選択実行
- 並列実行サポート: pytest-xdistによる分散テスト実行
技術的背景
pytestは2004年に開発が始まり、Pythonテストの標準unitTestモジュールの制約を解決するために設計されました。関数ベースのアプローチにより、クラス継承の複雑さを回避し、より直感的なテスト記述を可能にしています。フィクスチャシステムは依存性注入パターンを採用し、テストの再利用性と保守性を大幅に向上させています。
エコシステム
pytest-covによるカバレッジ測定、pytest-djangoによるDjango統合、pytest-asyncioによる非同期テスト、pytest-bddによるBDD(振る舞い駆動開発)サポートなど、フレームワークやライブラリとの豊富な統合オプションが提供されています。
メリット・デメリット
メリット
- 学習の容易さ: assert文とシンプルな関数で即座にテスト開始可能
- 高い生産性: フィクスチャとパラメータ化により効率的なテスト作成
- 優れた診断機能: 失敗時の詳細な情報表示でデバッグが容易
- 拡張性: プラグインシステムによる柔軟なカスタマイズ
- エンタープライズ対応: 大規模プロジェクトでの実績と安定性
- 活発なコミュニティ: 継続的な機能追加とサポート
デメリット
- 機能の多さ: 高度な機能の習得に時間が必要
- プラグイン依存: 特定機能にプラグインが必要な場合がある
- 実行速度: 小規模テストではunittestより若干重い場合がある
- 設定の複雑さ: 大規模プロジェクトでの設定管理が複雑になる可能性
参考ページ
書き方の例
Hello World(基本テスト)
# test_basic.py
def test_addition():
"""基本的な加算テスト"""
assert 2 + 3 == 5
def test_string_operations():
"""文字列操作のテスト"""
text = "Hello, pytest!"
assert text.startswith("Hello")
assert "pytest" in text
assert text.endswith("!")
# 実行: pytest test_basic.py
フィクスチャ使用例
# test_fixtures.py
import pytest
import tempfile
import os
@pytest.fixture
def sample_data():
"""テストデータを提供するフィクスチャ"""
return {
"users": ["Alice", "Bob", "Charlie"],
"scores": [85, 92, 78]
}
@pytest.fixture
def temp_file():
"""一時ファイルを作成・削除するフィクスチャ"""
# セットアップ
temp_fd, temp_path = tempfile.mkstemp()
os.close(temp_fd)
yield temp_path # テストに渡す
# クリーンアップ
if os.path.exists(temp_path):
os.unlink(temp_path)
def test_data_processing(sample_data):
"""フィクスチャを使用したテスト"""
users = sample_data["users"]
scores = sample_data["scores"]
assert len(users) == len(scores)
assert max(scores) == 92
assert "Alice" in users
def test_file_operations(temp_file):
"""ファイル操作のテスト"""
# ファイルに書き込み
with open(temp_file, 'w') as f:
f.write("Test content")
# ファイルから読み込み
with open(temp_file, 'r') as f:
content = f.read()
assert content == "Test content"
パラメータ化テスト
# test_parametrize.py
import pytest
@pytest.mark.parametrize("input_value,expected", [
(2, 4),
(3, 9),
(4, 16),
(5, 25),
])
def test_square(input_value, expected):
"""パラメータ化による複数ケーステスト"""
assert input_value ** 2 == expected
@pytest.mark.parametrize("text,should_be_valid", [
("[email protected]", True),
("[email protected]", True),
("invalid-email", False),
("@domain.com", False),
("user@", False),
])
def test_email_validation(text, should_be_valid):
"""メールアドレス検証のパラメータ化テスト"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
is_valid = bool(re.match(pattern, text))
assert is_valid == should_be_valid
# 複数パラメータの組み合わせ
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiplication(x, y):
"""複数パラメータの全組み合わせテスト"""
result = x * y
assert result > 0
assert result == x * y
例外テスト
# test_exceptions.py
import pytest
def divide(a, b):
"""除算関数(ゼロ除算チェック付き)"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class CustomError(Exception):
"""カスタム例外クラス"""
pass
def risky_function(value):
"""リスクのある処理をシミュレート"""
if value < 0:
raise CustomError("Negative values not allowed")
if value == 0:
raise ValueError("Zero is not valid")
return value * 2
def test_zero_division():
"""ゼロ除算例外のテスト"""
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
def test_successful_division():
"""正常な除算のテスト"""
result = divide(10, 2)
assert result == 5.0
def test_custom_exception():
"""カスタム例外のテスト"""
with pytest.raises(CustomError, match="Negative values not allowed"):
risky_function(-1)
def test_multiple_exceptions():
"""複数の例外パターンのテスト"""
# ValueError例外
with pytest.raises(ValueError, match="Zero is not valid"):
risky_function(0)
# CustomError例外
with pytest.raises(CustomError):
risky_function(-5)
def test_exception_info():
"""例外情報の詳細確認"""
with pytest.raises(ValueError) as excinfo:
divide(1, 0)
assert "Cannot divide by zero" in str(excinfo.value)
assert excinfo.type == ValueError
マーキング(カスタムマーカー)
# test_markers.py
import pytest
# カスタムマーカーの定義(pytest.ini または conftest.py で)
# [pytest]
# markers =
# slow: marks tests as slow
# integration: marks tests as integration tests
# unit: marks tests as unit tests
# smoke: marks tests as smoke tests
@pytest.mark.unit
def test_fast_calculation():
"""高速な単体テスト"""
assert 2 + 2 == 4
@pytest.mark.slow
def test_complex_calculation():
"""時間のかかるテスト"""
import time
time.sleep(0.1) # 重い処理をシミュレート
result = sum(range(1000))
assert result == 499500
@pytest.mark.integration
def test_database_connection():
"""統合テスト(模擬)"""
# データベース接続のシミュレート
connection_status = True
assert connection_status
@pytest.mark.smoke
def test_basic_functionality():
"""スモークテスト"""
assert True
@pytest.mark.parametrize("env", ["development", "staging", "production"])
@pytest.mark.integration
def test_environment_specific(env):
"""環境別テスト"""
assert env in ["development", "staging", "production"]
# 実行例:
# pytest -m "unit" # 単体テストのみ実行
# pytest -m "not slow" # 遅いテスト以外を実行
# pytest -m "integration or smoke" # 統合テストとスモークテストを実行
プラグイン活用
# conftest.py - プロジェクト共通設定
import pytest
import logging
# カスタムフィクスチャ
@pytest.fixture(scope="session")
def api_client():
"""APIクライアントのセッションフィクスチャ"""
class MockAPIClient:
def __init__(self):
self.base_url = "https://api.example.com"
self.connected = True
def get(self, endpoint):
return {"status": "success", "data": f"Data from {endpoint}"}
def post(self, endpoint, data):
return {"status": "created", "id": 123}
def close(self):
self.connected = False
client = MockAPIClient()
yield client
client.close()
@pytest.fixture
def logger():
"""ロガーフィクスチャ"""
logging.basicConfig(level=logging.INFO)
return logging.getLogger(__name__)
# カスタムマーカー設定
def pytest_configure(config):
"""pytest設定のカスタマイズ"""
config.addinivalue_line(
"markers", "api: mark test as API test"
)
config.addinivalue_line(
"markers", "database: mark test as database test"
)
# テストファイル: test_plugins.py
import pytest
@pytest.mark.api
def test_api_get(api_client, logger):
"""APIプラグインを使用したテスト"""
logger.info("Testing API GET request")
response = api_client.get("/users")
assert response["status"] == "success"
assert "data" in response
@pytest.mark.api
def test_api_post(api_client):
"""APIプラグインを使用したPOSTテスト"""
data = {"name": "New User", "email": "[email protected]"}
response = api_client.post("/users", data)
assert response["status"] == "created"
assert response["id"] == 123
# pytest-cov使用例(カバレッジ測定)
# pip install pytest-cov
# 実行: pytest --cov=mymodule --cov-report=html
# pytest-xdist使用例(並列実行)
# pip install pytest-xdist
# 実行: pytest -n 4 # 4プロセスで並列実行
# pytest-mock使用例(モック機能拡張)
# pip install pytest-mock
def test_with_mock(mocker):
"""pytest-mockを使用したモックテスト"""
mock_function = mocker.patch('mymodule.expensive_function')
mock_function.return_value = "mocked result"
# テスト実行
from mymodule import use_expensive_function
result = use_expensive_function()
assert result == "mocked result"
mock_function.assert_called_once()