testing-python について
Pythonプロジェクト向けのテストスキルで、pytest、CLI・エンドツーエンドテスト、コンテナテスト、モックサーバー、pytest-qtを扱います。実際のファイルシステムやHTTPサービス、制御可能なモックバイナリを使うテストを重視します。
主な機能
- 01pytestフィクスチャとテスト構成
- 02CLI・エンドツーエンドテストのパターン
- 03一時ディレクトリと環境分離
- 04pytest-httpserverによるAPI再現
- 05コンテナテストサービスとモックバイナリ
- 06非同期テストとpytest-qt
活用シーン
- 01Python CLIアプリの出力と終了コードの検証
- 02一時ディレクトリを使ったファイル操作のテスト
- 03実HTTPサーバーを介したAPI連携テスト
- 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 固有のテスト優先順位:
- CLI / E2E テスト —
subprocess.run(["uv", "run", "poe", "app", ...])で実際のコマンドを実行し、出力と終了コードを確認します - 統合テスト — パブリック API を介したコンポーネント間の連携を、実際の一時ディレクトリと pytest-httpserver で確認します
- ユニットテスト — 純粋なデータ変換関数をテストします
- 省略 — フレームワークの接着部分、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 固有のコーディング規則です。