Skip to content
cccskills
FreePublished on GitHub

python-testing

pytest、TDD方法論、フィクスチャ、モック、パラメタライズ、カバレッジ要件を使用したPythonテスト戦略。

The English summary is being prepared. Showing the original description.

Works withPostgreSQLMySQL
See how to install

Files included (1)

  • SKILL.md20.2 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Pythonテストパターン

pytest、TDD方法論、ベストプラクティスを使用したPythonアプリケーションの包括的なテスト戦略。

アクティベーション条件

  • 新しいPythonコードを書くとき(TDDに従う: レッド、グリーン、リファクタリング)
  • Pythonプロジェクトのテストスイートを設計するとき
  • Pythonのテストカバレッジをレビューするとき
  • テストインフラストラクチャをセットアップするとき

テストの基本哲学

テスト駆動開発(TDD)

常にTDDサイクルに従います:

  1. RED: 望ましい動作のために失敗するテストを書く
  2. GREEN: テストを通すための最小限のコードを書く
  3. REFACTOR: テストをグリーンに保ちながらコードを改善する
# Step 1: 失敗するテストを書く (RED)
def test_add_numbers():
    result = add(2, 3)
    assert result == 5

# Step 2: 最小限の実装を書く (GREEN)
def add(a, b):
    return a + b

# Step 3: 必要に応じてリファクタリング (REFACTOR)

カバレッジ要件

  • 目標: 80%以上のコードカバレッジ
  • クリティカルパス: 100%のカバレッジが必要
  • カバレッジの測定には pytest --cov を使用
pytest --cov=mypackage --cov-report=term-missing --cov-report=html

pytestの基礎

基本的なテスト構造

import pytest

def test_addition():
    """Test basic addition."""
    assert 2 + 2 == 4

def test_string_uppercase():
    """Test string uppercasing."""
    text = "hello"
    assert text.upper() == "HELLO"

def test_list_append():
    """Test list append."""
    items = [1, 2, 3]
    items.append(4)
    assert 4 in items
    assert len(items) == 4

アサーション

# 等値
assert result == expected

# 非等値
assert result != unexpected

# 真偽値
assert result  # Truthy
assert not result  # Falsy
assert result is True  # Exactly True
assert result is False  # Exactly False
assert result is None  # Exactly None

# メンバーシップ
assert item in collection
assert item not in collection

# 比較
assert result > 0
assert 0 <= result <= 100

# 型チェック
assert isinstance(result, str)

# 例外テスト(推奨アプローチ)
with pytest.raises(ValueError):
    raise ValueError("error message")

# 例外メッセージのチェック
with pytest.raises(ValueError, match="invalid input"):
    raise ValueError("invalid input provided")

# 例外属性のチェック
with pytest.raises(ValueError) as exc_info:
    raise ValueError("error message")
assert str(exc_info.value) == "error message"

フィクスチャ

基本的なフィクスチャの使用

import pytest

@pytest.fixture
def sample_data():
    """Fixture providing sample data."""
    return {"name": "Alice", "age": 30}

def test_sample_data(sample_data):
    """Test using the fixture."""
    assert sample_data["name"] == "Alice"
    assert sample_data["age"] == 30

セットアップ/ティアダウン付きフィクスチャ

@pytest.fixture
def database():
    """Fixture with setup and teardown."""
    # セットアップ
    db = Database(":memory:")
    db.create_tables()
    db.insert_test_data()

    yield db  # テストに提供

    # ティアダウン
    db.close()

def test_database_query(database):
    """Test database operations."""
    result = database.query("SELECT * FROM users")
    assert len(result) > 0

フィクスチャスコープ

# 関数スコープ(デフォルト) - 各テストで実行
@pytest.fixture
def temp_file():
    with open("temp.txt", "w") as f:
        yield f
    os.remove("temp.txt")

# モジュールスコープ - モジュールごとに1回実行
@pytest.fixture(scope="module")
def module_db():
    db = Database(":memory:")
    db.create_tables()
    yield db
    db.close()

# セッションスコープ - テストセッションごとに1回実行
@pytest.fixture(scope="session")
def shared_resource():
    resource = ExpensiveResource()
    yield resource
    resource.cleanup()

パラメータ付きフィクスチャ

@pytest.fixture(params=[1, 2, 3])
def number(request):
    """Parameterized fixture."""
    return request.param

def test_numbers(number):
    """Test runs 3 times, once for each parameter."""
    assert number > 0

複数フィクスチャの使用

@pytest.fixture
def user():
    return User(id=1, name="Alice")

@pytest.fixture
def admin():
    return User(id=2, name="Admin", role="admin")

def test_user_admin_interaction(user, admin):
    """Test using multiple fixtures."""
    assert admin.can_manage(user)

autouseフィクスチャ

@pytest.fixture(autouse=True)
def reset_config():
    """Automatically runs before every test."""
    Config.reset()
    yield
    Config.cleanup()

def test_without_fixture_call():
    # reset_config runs automatically
    assert Config.get_setting("debug") is False

共有フィクスチャ用conftest.py

# tests/conftest.py
import pytest

@pytest.fixture
def client():
    """Shared fixture for all tests."""
    app = create_app(testing=True)
    with app.test_client() as client:
        yield client

@pytest.fixture
def auth_headers(client):
    """Generate auth headers for API testing."""
    response = client.post("/api/login", json={
        "username": "test",
        "password": "test"
    })
    token = response.json["token"]
    return {"Authorization": f"Bearer {token}"}

パラメタライズ

基本的なパラメタライズ

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("PyThOn", "PYTHON"),
])
def test_uppercase(input, expected):
    """Test runs 3 times with different inputs."""
    assert input.upper() == expected

複数パラメータ

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    """Test addition with multiple inputs."""
    assert add(a, b) == expected

ID付きパラメタライズ

@pytest.mark.parametrize("input,expected", [
    ("valid@email.com", True),
    ("invalid", False),
    ("@no-domain.com", False),
], ids=["valid-email", "missing-at", "missing-domain"])
def test_email_validation(input, expected):
    """Test email validation with readable test IDs."""
    assert is_valid_email(input) is expected

パラメタライズドフィクスチャ

@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def db(request):
    """Test against multiple database backends."""
    if request.param == "sqlite":
        return Database(":memory:")
    elif request.param == "postgresql":
        return Database("postgresql://localhost/test")
    elif request.param == "mysql":
        return Database("mysql://localhost/test")

def test_database_operations(db):
    """Test runs 3 times, once for each database."""
    result = db.query("SELECT 1")
    assert result is not None

マーカーとテスト選択

カスタムマーカー

# 遅いテストをマーク
@pytest.mark.slow
def test_slow_operation():
    time.sleep(5)

# インテグレーションテストをマーク
@pytest.mark.integration
def test_api_integration():
    response = requests.get("https://api.example.com")
    assert response.status_code == 200

# ユニットテストをマーク
@pytest.mark.unit
def test_unit_logic():
    assert calculate(2, 3) == 5

特定のテストを実行

# 高速テストのみ実行
pytest -m "not slow"

# インテグレーションテストのみ実行
pytest -m integration

# インテグレーションまたは遅いテストを実行
pytest -m "integration or slow"

# ユニットだが遅くないテストを実行
pytest -m "unit and not slow"

pytest.iniでマーカーを設定

[pytest]
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests
    django: marks tests as requiring Django

モッキングとパッチング

関数のモック

from unittest.mock import patch, Mock

@patch("mypackage.external_api_call")
def test_with_mock(api_call_mock):
    """Test with mocked external API."""
    api_call_mock.return_value = {"status": "success"}

    result = my_function()

    api_call_mock.assert_called_once()
    assert result["status"] == "success"

戻り値のモック

@patch("mypackage.Database.connect")
def test_database_connection(connect_mock):
    """Test with mocked database connection."""
    connect_mock.return_value = MockConnection()

    db = Database()
    db.connect()

    connect_mock.assert_called_once_with("localhost")

例外のモック

@patch("mypackage.api_call")
def test_api_error_handling(api_call_mock):
    """Test error handling with mocked exception."""
    api_call_mock.side_effect = ConnectionError("Network error")

    with pytest.raises(ConnectionError):
        api_call()

    api_call_mock.assert_called_once()

コンテキストマネージャーのモック

@patch("builtins.open", new_callable=mock_open)
def test_file_reading(mock_file):
    """Test file reading with mocked open."""
    mock_file.return_value.read.return_value = "file content"

    result = read_file("test.txt")

    mock_file.assert_called_once_with("test.txt", "r")
    assert result == "file content"

autospecの使用

@patch("mypackage.DBConnection", autospec=True)
def test_autospec(db_mock):
    """Test with autospec to catch API misuse."""
    db = db_mock.return_value
    db.query("SELECT * FROM users")

    # This would fail if DBConnection doesn't have query method
    db_mock.assert_called_once()

モッククラスインスタンス

class TestUserService:
    @patch("mypackage.UserRepository")
    def test_create_user(self, repo_mock):
        """Test user creation with mocked repository."""
        repo_mock.return_value.save.return_value = User(id=1, name="Alice")

        service = UserService(repo_mock.return_value)
        user = service.create_user(name="Alice")

        assert user.name == "Alice"
        repo_mock.return_value.save.assert_called_once()

モックプロパティ

@pytest.fixture
def mock_config():
    """Create a mock with a property."""
    config = Mock()
    type(config).debug = PropertyMock(return_value=True)
    type(config).api_key = PropertyMock(return_value="test-key")
    return config

def test_with_mock_config(mock_config):
    """Test with mocked config properties."""
    assert mock_config.debug is True
    assert mock_config.api_key == "test-key"

非同期コードのテスト

pytest-asyncioによる非同期テスト

import pytest

@pytest.mark.asyncio
async def test_async_function():
    """Test async function."""
    result = await async_add(2, 3)
    assert result == 5

@pytest.mark.asyncio
async def test_async_with_fixture(async_client):
    """Test async with async fixture."""
    response = await async_client.get("/api/users")
    assert response.status_code == 200

非同期フィクスチャ

@pytest.fixture
async def async_client():
    """Async fixture providing async test client."""
    app = create_app()
    async with app.test_client() as client:
        yield client

@pytest.mark.asyncio
async def test_api_endpoint(async_client):
    """Test using async fixture."""
    response = await async_client.get("/api/data")
    assert response.status_code == 200

非同期関数のモック

@pytest.mark.asyncio
@patch("mypackage.async_api_call")
async def test_async_mock(api_call_mock):
    """Test async function with mock."""
    api_call_mock.return_value = {"status": "ok"}

    result = await my_async_function()

    api_call_mock.assert_awaited_once()
    assert result["status"] == "ok"

例外のテスト

期待される例外のテスト

def test_divide_by_zero():
    """Test that dividing by zero raises ZeroDivisionError."""
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_custom_exception():
    """Test custom exception with message."""
    with pytest.raises(ValueError, match="invalid input"):
        validate_input("invalid")

例外属性のテスト

def test_exception_with_details():
    """Test exception with custom attributes."""
    with pytest.raises(CustomError) as exc_info:
        raise CustomError("error", code=400)

    assert exc_info.value.code == 400
    assert "error" in str(exc_info.value)

副作用のテスト

ファイル操作のテスト

import tempfile
import os

def test_file_processing():
    """Test file processing with temp file."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
        f.write("test content")
        temp_path = f.name

    try:
        result = process_file(temp_path)
        assert result == "processed: test content"
    finally:
        os.unlink(temp_path)

pytestのtmp_pathフィクスチャによるテスト

def test_with_tmp_path(tmp_path):
    """Test using pytest's built-in temp path fixture."""
    test_file = tmp_path / "test.txt"
    test_file.write_text("hello world")

    result = process_file(str(test_file))
    assert result == "hello world"
    # tmp_path automatically cleaned up

tmpdirフィクスチャによるテスト

def test_with_tmpdir(tmpdir):
    """Test using pytest's tmpdir fixture."""
    test_file = tmpdir.join("test.txt")
    test_file.write("data")

    result = process_file(str(test_file))
    assert result == "data"

テストの整理

ディレクトリ構造

tests/
├── conftest.py                 # 共有フィクスチャ
├── __init__.py
├── unit/                       # ユニットテスト
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_utils.py
│   └── test_services.py
├── integration/                # インテグレーションテスト
│   ├── __init__.py
│   ├── test_api.py
│   └── test_database.py
└── e2e/                        # E2Eテスト
    ├── __init__.py
    └── test_user_flow.py

テストクラス

class TestUserService:
    """Group related tests in a class."""

    @pytest.fixture(autouse=True)
    def setup(self):
        """Setup runs before each test in this class."""
        self.service = UserService()

    def test_create_user(self):
        """Test user creation."""
        user = self.service.create_user("Alice")
        assert user.name == "Alice"

    def test_delete_user(self):
        """Test user deletion."""
        user = User(id=1, name="Bob")
        self.service.delete_user(user)
        assert not self.service.user_exists(1)

ベストプラクティス

やるべきこと

  • TDDに従う: コードの前にテストを書く(レッド-グリーン-リファクタリング)
  • 1つのことをテスト: 各テストは1つの動作を検証すべき
  • 説明的な名前を使用: test_user_login_with_invalid_credentials_fails
  • フィクスチャを使用: フィクスチャで重複を排除
  • 外部依存関係をモック: 外部サービスに依存しない
  • エッジケースをテスト: 空の入力、None値、境界条件
  • 80%以上のカバレッジを目指す: クリティカルパスに焦点を当てる
  • テストを高速に保つ: マークを使用して遅いテストを分離

やってはいけないこと

  • 実装をテストしない: 内部ではなく動作をテスト
  • テスト内で複雑な条件分岐を使わない: テストはシンプルに保つ
  • テストの失敗を無視しない: すべてのテストが通る必要がある
  • サードパーティコードをテストしない: ライブラリが動作することを信頼
  • テスト間で状態を共有しない: テストは独立すべき
  • テスト内で例外をキャッチしない: pytest.raises を使用
  • printステートメントを使わない: アサーションとpytest出力を使用
  • 脆すぎるテストを書かない: 過度に特定的なモックを避ける

一般的なパターン

APIエンドポイントのテスト(FastAPI/Flask)

@pytest.fixture
def client():
    app = create_app(testing=True)
    return app.test_client()

def test_get_user(client):
    response = client.get("/api/users/1")
    assert response.status_code == 200
    assert response.json["id"] == 1

def test_create_user(client):
    response = client.post("/api/users", json={
        "name": "Alice",
        "email": "alice@example.com"
    })
    assert response.status_code == 201
    assert response.json["name"] == "Alice"

データベース操作のテスト

@pytest.fixture
def db_session():
    """Create a test database session."""
    session = Session(bind=engine)
    session.begin_nested()
    yield session
    session.rollback()
    session.close()

def test_create_user(db_session):
    user = User(name="Alice", email="alice@example.com")
    db_session.add(user)
    db_session.commit()

    retrieved = db_session.query(User).filter_by(name="Alice").first()
    assert retrieved.email == "alice@example.com"

クラスメソッドのテスト

class TestCalculator:
    @pytest.fixture
    def calculator(self):
        return Calculator()

    def test_add(self, calculator):
        assert calculator.add(2, 3) == 5

    def test_divide_by_zero(self, calculator):
        with pytest.raises(ZeroDivisionError):
            calculator.divide(10, 0)

pytest設定

pytest.ini

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    --strict-markers
    --disable-warnings
    --cov=mypackage
    --cov-report=term-missing
    --cov-report=html
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests

pyproject.toml

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--strict-markers",
    "--cov=mypackage",
    "--cov-report=term-missing",
    "--cov-report=html",
]
markers = [
    "slow: marks tests as slow",
    "integration: marks tests as integration tests",
    "unit: marks tests as unit tests",
]

テストの実行

# すべてのテストを実行
pytest

# 特定のファイルを実行
pytest tests/test_utils.py

# 特定のテストを実行
pytest tests/test_utils.py::test_function

# 詳細出力で実行
pytest -v

# カバレッジ付きで実行
pytest --cov=mypackage --cov-report=html

# 高速テストのみ実行
pytest -m "not slow"

# 最初の失敗まで実行
pytest -x

# N回の失敗で停止
pytest --maxfail=3

# 前回失敗したテストを実行
pytest --lf

# パターンでテストを実行
pytest -k "test_user"

# 失敗時にデバッガーで実行
pytest --pdb

クイックリファレンス

パターン使用法
pytest.raises()期待される例外のテスト
@pytest.fixture()再利用可能なテストフィクスチャの作成
@pytest.mark.parametrize()複数入力でテストを実行
@pytest.mark.slow遅いテストをマーク
pytest -m "not slow"遅いテストをスキップ
@patch()関数とクラスのモック
tmp_path フィクスチャ自動一時ディレクトリ
pytest --covカバレッジレポートの生成
assertシンプルで読みやすいアサーション

覚えておくこと: テストもコードです。クリーンで読みやすく保守しやすいテストを書きましょう。良いテストはバグを捕捉し、優れたテストはバグを防止します。

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

コーディングエージェント(Claude Code、Aider、Codexなど)のカスタムタスクによる直接比較。合格率、コスト、時間、一貫性メトリクスを測定

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AIエージェントのアクションスペース、ツール定義、オブザベーションフォーマットを設計・最適化し、タスク完了率を向上させる

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

評価ファーストの実行、分解、コスト考慮型モデルルーティングによるエージェンティックエンジニアとしての運用

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AIエージェントが実装出力の大部分を生成するチームのためのエンジニアリング運用モデル

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AI支援開発のためのリグレッションテスト戦略。データベース依存なしのサンドボックスモードAPIテスト、自動バグチェックワークフロー、同一モデルがコードの作成とレビューを行う際のAIの盲点を検出するパターン。

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

AndroidおよびKotlin Multiplatformプロジェクトのためのクリーンアーキテクチャパターン — モジュール構成、依存関係ルール、UseCase、Repository、データレイヤーパターン

The English summary is being prepared. Showing the original description.

hage-oyaji/everything-claude-code-jp-local3Updated Mar 29, 2026

See all skills by hage-oyaji

Report a problem with this skill