pytest-coder について
pytest-coderは、pytestを使った保守しやすいPythonテストの設計と作成を支援するスキルです。振る舞いの検証、分離、フィクスチャ、パラメータ化、モック、非同期テスト、境界ケースを重視します。
主な機能
- 01Arrange-Act-Assertのテスト構成
- 02フィクスチャとプロジェクト構成
- 03パラメータ化テストとマーカー
- 04外部依存のモック
- 05非同期処理と例外のテスト
- 06分離性と境界ケースの確認
活用シーン
- 01ユニットテストと統合テストの整理
- 02パラメータ化で複数の入力をテスト
- 03I/Oをモックしてユニットテストを高速化
- 04非同期コードと例外をテスト
- 05テストの分離性と境界ケースを確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill pytest-coderインストール後、Claude Code で/pytest-coderを入力して読み込まれたか確認します。
詳細
Pytest コーダー
中核となる理念
| 原則 | 適用方法 |
|---|---|
| AAA パターン | すべてのテストで Arrange-Act-Assert を実施 |
| 実装より振る舞い | コードの動作をテストし、実装方法はテストしません |
| 独立性 | テストは互いに独立している必要があります |
| 高速なテスト | I/O をモックし、データベースへのアクセスを最小限にします |
| 説明的な名前 | テスト名でシナリオを説明します |
| カバレッジ | 正常系とエッジケースの両方をテストします |
プロジェクト構成
tests/
├── conftest.py # Shared fixtures
├── unit/ # Unit tests (fast, isolated)
│ ├── test_models.py
│ └── test_services.py
├── integration/ # Integration tests (real dependencies)
│ └── test_api.py
└── fixtures/ # Test data files
└── sample_data.json
基本的なパターン
基本的なテスト構造
import pytest
from myapp.services import UserService
class TestUserService:
"""Tests for UserService."""
def test_create_user_with_valid_data(self, user_service):
# Arrange
user_data = {"email": "[email protected]", "name": "Test User"}
# Act
result = user_service.create(user_data)
# Assert
assert result.email == "[email protected]"
assert result.id is not None
def test_create_user_with_duplicate_email_raises_error(self, user_service, existing_user):
# Arrange
user_data = {"email": existing_user.email, "name": "Another User"}
# Act & Assert
with pytest.raises(ValueError, match="Email already exists"):
user_service.create(user_data)
フィクスチャ
# conftest.py
import pytest
from myapp.database import get_db
from myapp.services import UserService
@pytest.fixture
def db():
"""Provide a clean database session."""
session = get_db()
yield session
session.rollback()
@pytest.fixture
def user_service(db):
"""Provide UserService instance."""
return UserService(db)
@pytest.fixture
def sample_user():
"""Provide sample user data."""
return {"email": "[email protected]", "name": "Test User", "password": "secret123"}
@pytest.fixture
def existing_user(db, sample_user):
"""Create and return an existing user."""
from myapp.models import User
user = User(**sample_user)
db.add(user)
db.commit()
return user
パラメーター化テスト
import pytest
@pytest.mark.parametrize("input_email,expected_valid", [
("[email protected]", True),
("[email protected]", True),
("invalid-email", False),
("missing@domain", False),
("", False),
])
def test_email_validation(input_email, expected_valid):
from myapp.validators import is_valid_email
assert is_valid_email(input_email) == expected_valid
@pytest.mark.parametrize("status,expected_message", [
("pending", "Order is being processed"),
("shipped", "Order has been shipped"),
("delivered", "Order has been delivered"),
], ids=["pending-status", "shipped-status", "delivered-status"])
def test_order_status_message(status, expected_message):
from myapp.orders import get_status_message
assert get_status_message(status) == expected_message
モック
from unittest.mock import Mock, patch, AsyncMock
def test_send_email_calls_smtp(user_service):
# Mock external dependency
with patch("myapp.services.smtp_client") as mock_smtp:
mock_smtp.send.return_value = True
user_service.send_welcome_email("[email protected]")
mock_smtp.send.assert_called_once_with(
to="[email protected]",
subject="Welcome!",
)
def test_payment_processing_handles_failure():
mock_gateway = Mock()
mock_gateway.charge.side_effect = PaymentError("Card declined")
service = PaymentService(gateway=mock_gateway)
with pytest.raises(PaymentError):
service.process_payment(amount=100)
非同期テスト
import pytest
@pytest.mark.asyncio
async def test_async_fetch_user(user_service):
# Arrange
user_id = 1
# Act
user = await user_service.get_by_id(user_id)
# Assert
assert user.id == user_id
@pytest.fixture
async def async_db():
"""Async database session fixture."""
from myapp.database import async_session
async with async_session() as session:
yield session
await session.rollback()
# Mock async functions
@pytest.mark.asyncio
async def test_async_external_api():
with patch("myapp.client.fetch_data", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = {"status": "ok"}
result = await fetch_and_process()
assert result["status"] == "ok"
例外のテスト
import pytest
def test_divide_by_zero_raises_error():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_invalid_input_raises_with_message():
with pytest.raises(ValueError, match="must be positive"):
process_amount(-100)
def test_exception_attributes():
with pytest.raises(CustomError) as exc_info:
risky_operation()
assert exc_info.value.code == "E001"
assert "failed" in str(exc_info.value)
フィクスチャのスコープ
| スコープ | ライフサイクル | 使用例 |
|---|---|---|
function | テストごと(デフォルト) | ほとんどのフィクスチャ |
class | テストクラスごと | クラス内で共有するセットアップ |
module | モジュールごと | モジュール内で共有するコストの高いセットアップ |
session | テスト実行全体 | データベース接続、サーバー |
@pytest.fixture(scope="session")
def database_engine():
"""Create engine once for entire test session."""
engine = create_engine(TEST_DATABASE_URL)
yield engine
engine.dispose()
@pytest.fixture(scope="function")
def db_session(database_engine):
"""Create fresh session per test."""
connection = database_engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
マーカー
# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow",
"integration: marks integration tests",
"unit: marks unit tests",
]
# Usage
@pytest.mark.slow
def test_complex_calculation():
...
@pytest.mark.integration
def test_database_connection():
...
# Run specific markers
# pytest -m "not slow"
# pytest -m "unit"
品質チェックリスト
- すべてのテストで AAA パターン(Arrange-Act-Assert)を使用する
- シナリオを説明する、説明的なテスト名を付ける
- 共通のセットアップにはフィクスチャを使用する
- 複数の入力にはパラメーター化テストを使用する
- 外部依存関係にはモックを使用する
- 正常系をテストする
- エラーケースをテストする
- エッジケースを網羅する
- 非同期テストでは
@pytest.mark.asyncioを使用する - テスト間に依存関係がない
- カバレッジが 90% を超えている
アンチパターン
| アンチパターン | 問題点 | 修正方法 |
|---|---|---|
| テストが実行順序に依存する | 不安定で、デバッグが困難です | フィクスチャを使用し、分離します |
| 実装をテストする | テストが壊れやすくなります | 振る舞いをテストします |
| アサーションが多すぎる | 失敗箇所を特定しにくくなります | テストごとにアサーションを 1 つにします |
| エラーケースのテストがない | カバレッジが不足します | 例外を明示的にテストします |
| 遅い単体テスト | フィードバックが遅くなります | I/O をモックし、インメモリ DB を使用します |
コメント
Markdown 対応