mcpField
T
testing-python

testing-python

github.com/quick-brown-foxxxテスト・品質138 回インストール6 週間前に更新
リポジトリ

testing-python について

Pythonプロジェクト向けのテストスキルで、pytest、CLI・エンドツーエンドテスト、コンテナテスト、モックサーバー、pytest-qtを扱います。実際のファイルシステムやHTTPサービス、制御可能なモックバイナリを使うテストを重視します。

主な機能

  1. 01pytestフィクスチャとテスト構成
  2. 02CLI・エンドツーエンドテストのパターン
  3. 03一時ディレクトリと環境分離
  4. 04pytest-httpserverによるAPI再現
  5. 05コンテナテストサービスとモックバイナリ
  6. 06非同期テストとpytest-qt

活用シーン

  1. 01Python CLIアプリの出力と終了コードの検証
  2. 02一時ディレクトリを使ったファイル操作のテスト
  3. 03実HTTPサーバーを介したAPI連携テスト
  4. 04サービス依存テスト用のコンテナ環境構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/quick-brown-foxxx/coding_rules_python --skill testing-python

インストール後、Claude Code で/testing-pythonを入力して読み込まれたか確認します。

詳細

Python のテスト

前提条件

このスキルは、myai の high-level-testing-strategy、test-driven-development、manual-testing を拡張します。まず、これらを読み込んでください。using-my-skills と engineering-principles は、myai のブートストラップで読み込み済みであることを前提とします。

テスト全般の方針(カバレッジより信頼性、ユニットテストより E2E テスト、モックより実物、パレートの法則)については、myai の engineering-principles と high-level-testing-strategy を参照してください。TDD のレッド・グリーン・リファクタリングのワークフローについては、test-driven-development を参照してください。手動検証のパターンについては、manual-testing を参照してください。このスキルでは、pytest のフィクスチャ、CLI/E2E テストのパターン、コンテナ化テスト、モックサーバー、pytest-qt など、Python 固有のテストツールとパターンのみを扱います。


テストの計画と優先順位

Python 固有のテスト優先順位:

  1. CLI / E2E テスト — subprocess.run(["uv", "run", "poe", "app", ...]) で実際のコマンドを実行し、出力と終了コードを確認します
  2. 統合テスト — パブリック API を介したコンポーネント間の連携を、実際の一時ディレクトリと pytest-httpserver で確認します
  3. ユニットテスト — 純粋なデータ変換関数をテストします
  4. 省略 — フレームワークの接着部分、UI レイアウト、単純な getter
ディレクトリ構成
tests/
├── unit/              # Pure function tests
├── integration/       # CLI tests, component interaction
├── fixtures/          # Shared test data and helpers
└── conftest.py        # Shared fixtures
pyproject.toml の設定
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
addopts = ["-n", "auto", "--dist", "worksteal"]

markers = [
    "unit: unit tests",
    "integration: integration tests",
]
依存関係
[dependency-groups]
dev = [
    "pytest>=9.0.1",
    "pytest-xdist>=3.5.0",
    "pytest-cov>=7.0.0",
    "pytest-asyncio>=1.3.0",
    # "pytest-qt>=4.5.0",          # For Qt apps
    # "pytest-httpserver>=1.1.0",  # For HTTP mocking
]

テストの例

CLI テストの例
import subprocess

def test_list_profiles_empty() -> None:
    result = subprocess.run(
        ["uv", "run", "poe", "app", "list"],
        capture_output=True, text=True,
    )
    assert result.returncode == 0
    assert "No profiles found" in result.stdout

def test_create_and_list_profile(tmp_path: Path) -> None:
    env = {**os.environ, "APP_DATA_DIR": str(tmp_path)}
    subprocess.run(
        ["uv", "run", "poe", "app", "create", "test-profile"],
        env=env, check=True,
    )
    result = subprocess.run(
        ["uv", "run", "poe", "app", "list"],
        capture_output=True, text=True, env=env,
    )
    assert "test-profile" in result.stdout
Result パターンのテスト例
def test_load_config_missing_file() -> None:
    result = load_config(Path("/nonexistent"))
    assert result.is_err
    assert "not found" in result.unwrap_err()

def test_load_config_valid() -> None:
    result = load_config(Path("tests/fixtures/valid_config.yaml"))
    assert result.is_ok
    config = result.unwrap()
    assert config.name == "test"
非同期テストの例
@pytest.mark.asyncio
async def test_fetch_data() -> None:
    result = await fetch_data("https://httpbin.org/get")
    assert result.is_ok

フィクスチャ

一時ディレクトリ
@pytest.fixture
def app_data_dir(tmp_path: Path) -> Path:
    data_dir = tmp_path / "data"
    data_dir.mkdir()
    return data_dir
環境変数の上書き(Linux/XDG の例)
@pytest.fixture
def isolated_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, Path]:
    config = tmp_path / "config"
    data = tmp_path / "data"
    config.mkdir()
    data.mkdir()
    monkeypatch.setenv("XDG_CONFIG_HOME", str(config))
    monkeypatch.setenv("XDG_DATA_HOME", str(data))
    return config, data

クロスプラットフォームで使える代替方法として、XDG 環境変数の代わりに、テスト対象のコードへディレクトリを渡すか、platformdirs.user_*_dir の呼び出しを monkeypatch します。

サンプルデータ
@pytest.fixture
def sample_audio_16khz() -> np.ndarray:
    return np.zeros(16000, dtype=np.float32)  # 1 second of silence
HTTP サーバーのモック(パッチではなく実サーバー)
@pytest.fixture
def mock_api(httpserver: HTTPServer) -> HTTPServer:
    httpserver.expect_request("/api/data").respond_with_json({"status": "ok"})
    return httpserver

def test_fetch_from_api(mock_api: HTTPServer) -> None:
    result = fetch_data(mock_api.url_for("/api/data"))
    assert result.is_ok

テストの実行

テストはデフォルトで並列に実行されます(addopts による -n auto)。-n0(逐次実行)または -n4(指定した数で実行)で上書きできます。

uv run poe test                    # All tests (parallel, auto workers)
uv run pytest tests/unit/          # Unit only
uv run pytest -n0                  # Force sequential (debugging)
uv run pytest --cov                # With coverage report

テストの分離

Python 固有の方法として、ファイルには tmp_path、環境変数には monkeypatch、後処理には yield フィクスチャを使います。


不安定なテスト

不安定なテストへの一般的な対処方法については、myai の systematic-debugging を参照してください。


カバレッジのガイドライン

カバレッジ全般の方針(目標値ではなくガイドライン)については、myai の engineering-principles を参照してください。 Python 固有の目安:

領域ガイドライン
コアビジネスロジック>70%
CLI コマンド>70%
UI コンポーネント>40%
ユーティリティ必要に応じて

テストの検証

テスト品質の一般的なレビュー手順については、doing-code-review と verification-before-completion を参照してください。


大規模なテスト

軽量なテストでは不十分な場合に使用します。基本方針は同じですが、インフラの複雑さが増します。

プラットフォームに関する注意: 以下の DBus、Docker/コンテナ、POSIX モックバイナリのパターンは、Linux/ホスト環境に固有です。Windows でもテストスイートが成功するよう、sys.platform / pytest.mark.skipif で実行を制御してください。クロスプラットフォームでパスを分離する場合は、tmp_path と platformdirs の monkeypatch を優先してください。

基本的な考え方
代わりに...使用するもの...
@patch("requests.get")実 HTTP サーバー(pytest-httpserver またはカスタムサーバー)
@patch("subprocess.run")実物のように動作する、軽量なカスタムバイナリ
DB に対する unittest.mock.Mock()コンテナ内の実データベース
monkeypatch したファイル操作tmp_path またはコンテナボリューム内の実ファイルシステム
モック化したシステムサービス(DBus)テスト用の実デーモンインスタンス
コンテナ化されたテスト環境
tests/
├── containers/
│   ├── Dockerfile.test-env        # Base test environment
│   ├── Dockerfile.mock-api        # Mock API server
│   ├── docker-compose.test.yml    # Orchestration
│   └── mock-bins/                 # Custom mock binaries
│       ├── mock-telegram          # Fake Telegram Desktop
│       └── mock-ffmpeg            # Fake ffmpeg (returns predefined output)
├── integration/
│   └── test_with_containers.py
└── conftest.py                    # Container lifecycle fixtures
テストサービス用 Docker Compose
# tests/containers/docker-compose.test.yml
services:
  mock-api:
    build:
      context: .
      dockerfile: Dockerfile.mock-api
    ports:
      - "18080:8080"

  test-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: test
      POSTGRES_PASSWORD: test
    ports:
      - "15432:5432"
コンテナのライフサイクル用フィクスチャ
@pytest.fixture(scope="session")
def test_services():
    """Start all test containers, yield, then tear down."""
    compose_file = Path(__file__).parent / "containers" / "docker-compose.test.yml"
    subprocess.run(
        ["docker-compose", "-f", str(compose_file), "up", "-d", "--wait"],
        check=True,
    )
    yield
    subprocess.run(
        ["docker-compose", "-f", str(compose_file), "down", "-v"],
        check=True,
    )
モックバイナリ

subprocess.run() にパッチを当てる代わりに、予測可能な動作をする実バイナリを用意します:

#!/bin/env python3
# tests/containers/mock-bins/mock-telegram
import sys, time, os

print("Telegram Desktop Mock v1.0")
print(f"Working directory: {os.getcwd()}")

if "-many" in sys.argv and "-workdir" in sys.argv:
    print(f"Mock Telegram started in {sys.argv[sys.argv.index('-workdir') + 1]}")
    time.sleep(int(os.environ.get("MOCK_TELEGRAM_LIFETIME", "5")))
    sys.exit(0)

print("Unknown arguments", sys.argv, file=sys.stderr)
sys.exit(1)
@pytest.fixture
def mock_telegram_bin(tmp_path: Path) -> Path:
    mock_bin = tmp_path / "telegram"
    mock_bin.write_text(MOCK_TELEGRAM_SCRIPT)
    mock_bin.chmod(0o755)
    return mock_bin

async def test_start_instance(mock_telegram_bin: Path) -> None:
    result = await start_instance(profile, binary_path=mock_telegram_bin)
    assert result.is_ok
    pid = result.unwrap()
    assert pid > 0
状態を保持するモック API

リクエスト間で状態を保持する必要がある API の場合:

from http.server import HTTPServer, BaseHTTPRequestHandler
import threading, json

class MockAPIHandler(BaseHTTPRequestHandler):
    profiles: dict[str, dict[str, str | int | bool]] = {}

    def do_POST(self) -> None:
        if self.path == "/api/profiles":
            data = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
            self.profiles[data["id"]] = data
            self.send_response(201)
            self.end_headers()
            self.wfile.write(json.dumps(data).encode())

    def do_GET(self) -> None:
        if self.path == "/api/profiles":
            self.send_response(200)
            self.end_headers()
            self.wfile.write(json.dumps(list(self.profiles.values())).encode())

@pytest.fixture(scope="session")
def mock_api() -> Generator[str, None, None]:
    server = HTTPServer(("127.0.0.1", 0), MockAPIHandler)
    port = server.server_address[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    yield f"http://127.0.0.1:{port}"
    server.shutdown()
実サービスのテスト(DBus)
@pytest.fixture(scope="session")
def dbus_session() -> Generator[str, None, None]:
    """Start a real DBus session daemon for tests."""
    process = subprocess.Popen(
        ["dbus-daemon", "--session", "--print-address", "--nofork"],
        stdout=subprocess.PIPE,
    )
    address = process.stdout.readline().decode().strip()
    os.environ["DBUS_SESSION_BUS_ADDRESS"] = address
    yield address
    process.terminate()
    process.wait()

関連する myai スキル

  • high-level-testing-strategy — 親スキルです。BDD を優先したテスト計画、振る舞いのシナリオ、自動化の範囲、モックの境界を扱います。
  • test-driven-development — 親スキルです。自動テストのレッド・グリーン・リファクターのワークフローを扱います。
  • manual-testing — 親スキルです。環境の事前確認、分離、証拠の記録を含む、現実的な手動検証を扱います。
  • architecting-test-infra — テストインフラ、フィクスチャ、状態の分離、シードレイヤーのセットアップに使用します。
  • engineering-principles — 言語に依存しないテストの理念です。網羅率より信頼性を、モックより実物を重視します。
  • writing-python-code — テストコードにも適用される、Python 固有のコーディング規則です。

コメント