test-harness について
test-harnessは、Pythonソースコードから実行可能なpytestテストを作成するスキルです。テストの作成・保守を行う開発者向けに、通常・境界・エラー・非同期ケースと、プロジェクトの慣習に沿ったフィクスチャやモックを整理します。
主な機能
- 01関数と依存関係の分析
- 02通常・境界・エラーケースの網羅
- 03フィクスチャとモックの設計
- 04非同期テストのガイダンス
- 05簡易・標準・包括モード
活用シーン
- 01新しいモジュールのpytestテスト作成
- 02既存テストと重複せずカバレッジを拡張
- 03外部依存や非同期コードのテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mathews-tom/ armory --skill test-harnessインストール後、Claude Code で/test-harnessを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/cases.yamlevals/fixtures/sample_module.pyreferences/async-testing.mdcoverage-targets.mdfixture-design.mdmock-strategies.mdpytest-patterns.md詳細
テストハーネス
ソースコードから包括的で実行可能な pytest ファイルを生成する、体系的なテストスイート生成機能です。関数シグネチャ、依存関係グラフ、複雑性の高い箇所を分析し、正常系、境界条件、エラー状態、非同期フローを網羅するテストを、適切なスコープのフィクスチャと目的を絞ったモックを用いて生成します。
参照ファイル
| ファイル | 内容 | 読み込むタイミング |
|---|---|---|
references/pytest-patterns.md | フィクスチャのスコープ、parametrize、マーク、conftest の構成、組み込みフィクスチャ | 常に |
references/mock-strategies.md | モックの判断ツリー、パッチの境界、アサーション、アンチパターン | 対象に外部依存関係がある場合 |
references/async-testing.md | pytest-asyncio のモード、イベントループのフィクスチャ、非同期モック | 対象に非同期コードが含まれる場合 |
references/fixture-design.md | ファクトリーフィクスチャ、yield によるティアダウン、スコープの選択、構成 | テストに複雑なセットアップが必要な場合 |
references/coverage-targets.md | カバレッジのしきい値テーブル、ブランチと行の違い、pytest-cov の設定、除外パターン | カバレッジ評価が求められる場合 |
前提条件
- pytest >= 7.0
- Python >= 3.10
- pytest-asyncio — 非同期テストを生成する場合のみ必要です
- pytest-mock — オプションです。
unittest.mockの代替としてmockerフィクスチャを提供します
ワークフロー
フェーズ 1:事前調査
テストを 1 つも書く前に、対象コードのモデルを構築します。
- 対象範囲を特定する — どの関数、クラス、モジュールをテストする必要がありますか?指定がない場合は、最近の変更を確認します:
git diff --name-only HEAD~5 - 関数シグネチャを読む — パラメーター、型、戻り値の型、デフォルト値を確認します。各パラメーターがテストの観点になります。
- 依存関係をマッピングする — どの呼び出しが外部システム(DB、API、ファイルシステム、クロック)に接続していますか?これらがモックの候補です。
- 複雑性の高い箇所を検出する — 分岐数が多い関数、ネストが深い関数、戻り値の経路が複数ある関数には、より多くのテストケースが必要です。
- 既存のテストを確認する — テストがすでに存在する場合は、そのカバレッジを把握します。重複させず、拡張してください。
- プロジェクトの規約を読む — CLAUDE.md、conftest.py、pytest.ini/pyproject.toml を確認し、すでに使用されているフィクスチャ、マーカー、テスト構成のパターンを把握します。
フェーズ 2:テストケースの列挙
テスト対象の各関数について、次の 4 つのカテゴリに沿ってケースを列挙します。
| カテゴリ | テストする内容 | 例 |
|---|---|---|
| 正常系 | 期待する入力で期待する出力が得られること | add(2, 3) が 5 を返す |
| 境界条件 | 有効な入力の範囲内にある境界値 | 空文字列、ゼロ、最大の整数、単一要素 |
| エラー | 無効な入力で適切な例外が発生すること | str が期待される箇所に None、負のインデックス |
| 状態 | 状態遷移で正しい副作用が発生すること | オブジェクトが pending から active に遷移する |
各ケースについて、次の項目を記録します。
- 入力値(抽象的な値ではなく、具体的な値)
- 期待する出力または例外
- 必要なセットアップ(フィクスチャ)
- 必要なモック(抑制する外部呼び出し)
テストロジックが同じで、入力値や出力値だけが異なるケースは、パラメーター化します。
フェーズ 3:フィクスチャの設計
-
共通のセットアップを特定する — 3 件以上のテストで同じオブジェクトが必要な場合は、フィクスチャに切り出します。
-
スコープを選択する — セットアップの重複を避けられる最も狭いスコープを使用します。
スコープ 使用するケース 例 functionデフォルト。各テストで新しい状態を取得します ほとんどのユニットテスト classクラス内のテストで負荷の高いセットアップを共有します テストクラスごとの DB 接続 moduleファイル内のすべてのテストでセットアップを共有します 読み込み済みの設定ファイル sessionテスト実行全体でセットアップを共有します Docker コンテナの起動 -
ティアダウンを設計する — クリーンアップが必要な場合は、
yieldフィクスチャを使用します。テスト後に副作用(一時ファイル、DB の行、モンキーパッチ)を決して残さないでください。 -
conftest の候補を特定する — 複数のテストファイルで使われるフィクスチャは、
conftest.pyに配置します。1 つのファイルだけで使われるフィクスチャは、そのファイル内に配置します。
フェーズ 4:モック戦略
-
モックする対象を決める — 外部依存関係のみをモックします。
- ネットワーク呼び出し(API、データベース、メッセージキュー)
- ファイルシステム操作(I/O ではなくロジックをテストする場合)
- 時間に依存する動作(
datetime.now、time.sleep) - ランダムまたは非決定的な動作
-
モックしない対象を決める — 以下は決してモックしないでください。
- テスト対象の関数
- 対象から呼び出される純粋関数(対象を通してテストします)
- データ構造と値オブジェクト
-
モックのレベルを選択する — 定義箇所ではなく、テスト対象モジュールのインポート境界でパッチを適用します。
@patch('requests.get')ではなく、@patch('mymodule.requests.get')を使います。 -
モックのアサーションを追加する — 各モックで、期待する引数を使って期待する回数だけ呼び出されたことをアサートします。アサーションのないモックは、カバレッジの穴になります。
フェーズ 5:出力
以下の構成に従ってテストファイルを生成します。
- インポート(pytest、モック、対象モジュール)
- 定数とテストデータ
- フィクスチャ(スコープ順:session > module > class > function)
- 対象関数ごとにグループ化したテストクラスまたは関数
- 該当する場合はパラメーター化テスト
出力形式
# tests/test_{module}.py
import pytest
from unittest.mock import Mock, patch, MagicMock
from {module} import {target_function, TargetClass}
# ============================================================
# Fixtures
# ============================================================
@pytest.fixture
def valid_input():
"""Standard valid input for happy path tests."""
return {concrete values}
@pytest.fixture
def mock_database():
"""Mock database connection."""
with patch("{module}.db_connection") as mock_db:
mock_db.query.return_value = [{expected data}]
yield mock_db
# ============================================================
# {target_function} Tests
# ============================================================
class TestTargetFunction:
"""Tests for {target_function}."""
def test_happy_path(self, valid_input):
"""Returns expected result for valid input."""
result = target_function(valid_input)
assert result == {expected}
@pytest.mark.parametrize(
"input_val, expected",
[
({boundary_1}, {expected_1}),
({boundary_2}, {expected_2}),
({boundary_3}, {expected_3}),
],
ids=["empty", "single", "maximum"],
)
def test_boundary_conditions(self, input_val, expected):
"""Handles boundary inputs correctly."""
assert target_function(input_val) == expected
def test_invalid_input_raises(self):
"""Raises TypeError for invalid input."""
with pytest.raises(TypeError, match="expected str"):
target_function(None)
def test_external_call(self, mock_database):
"""Calls database with correct query."""
target_function("lookup_key")
mock_database.query.assert_called_once_with("SELECT * FROM t WHERE key = %s", ("lookup_key",))
スコープの設定
| モード | スコープ | 深さ | 使用するケース |
|---|---|---|---|
quick | 単一関数 | ハッピーパス + 1 つのエラーケース | 迅速な反復、TDD のレッド・グリーンサイクル |
standard | ファイルまたはクラス | ハッピーパス + 境界値 + エラー + モック | ほとんどのリクエストでのデフォルト |
comprehensive | モジュールまたはパッケージ | すべてのカテゴリ + 非同期 + パラメーター化マトリックス | リリース前、重要なパスのコード |
キャリブレーションルール
- テストの分離は必須です。 すべてのテストは単独で、またどの順序で実行してもパスしなければなりません。 ほかのテストの副作用に依存するテストがあってはなりません。
- モックを適切に使います。 外部依存関係をモックし、内部ロジックはモックしないでください。過剰なモックは、コードが壊れていてもテストがパスする原因になります。モックが不十分だと、ネットワークがダウンしたときにテストが失敗します。
- 抽象的な値より具体的な値を使います。 テストデータにはプレースホルダーではなく、具体的な値を使ってください。
"test_email"ではなく"[email protected]"、"some_number"ではなく42を使います。具体的な値なら、抽象的なプレースホルダーでは見逃される型の不一致を検出できます。 - テストごとにアサーションの焦点を1つにします。 テストでは1つの動作を検証します。異なる側面であっても、同じ動作を検証するアサーション(例:戻り値と副作用)なら複数あってもかまいませんが、無関係な動作を検証してはいけません。
- 重複させず、パラメーター化します。 入力値と出力値だけが異なるテストが2つある場合は、
@pytest.mark.parametrizeでまとめてください。読みやすいテスト名にするため、idsを使います。 - プロジェクトの慣例に合わせます。 プロジェクトで
conftest.pyのフィクスチャ、クラスベースのテスト、または特定のマーカーが使われている場合は、そのパターンに従ってください。競合するテストスタイルを導入しないでください。
エラー処理
| 問題 | 解決策 |
|---|---|
| 対象関数に型ヒントがない | 使用パターン、デフォルト値、docstring から型を推測します。不確かな点はテストの docstring に記載します。 |
| 対象に深くネストされた依存関係がある | テスト対象の関数に最も近い境界でモックします。推移的依存関係を個別にモックしないでください。 |
| 既存のテストインフラストラクチャがない(conftest、pytest 設定なし) | テストファイルと同じ場所に最小限の conftest.py を生成します。追加したことを出力に記載します。 |
| 対象コードをテストできない(グローバル状態、隠れた依存関係) | 設計上の問題を出力で指摘します。テスト可能な範囲のテストを生成し、テストしやすくするリファクタリングを提案します。 |
| 非同期コードが検出されたが pytest-asyncio がインストールされていない | 依存関係の要件を記載します。@pytest.mark.asyncio を使った非同期テストのスタブを生成し、インストールするよう案内します。 |
| 対象モジュールをインポートできない | インポートエラーを報告します。インポートできないコードのテストは生成しないでください。 |
テストを生成しない場合
以下の場合は、ユーザーの依頼を再検討してください。
- コードが自動生成されたもの(protobuf、OpenAPI クライアント、ORM モデル)の場合 — 生成器またはスキーマをテストし、出力されたコードはテストしないでください
- 依頼が UI/E2E テストの場合 — このスキルで生成するのは単体テストと統合テストのみです
- テスト可能な明確な動作がない場合(純粋な設定、定数の定義)
- サードパーティーライブラリのコードのテストを求められた場合 — ライブラリ自体ではなく、そのライブラリの使い方をテストしてください
よくある正当化
| よくある正当化 | 実態 |
|---|---|
| 「手動テストで十分です」 | 手動テストは CI で実行されず、リグレッションを検出できず、コードベースの拡大にも対応できません |
| 「このコードはテストするほど複雑ではありません」 | シンプルなコードもやがて複雑になります。テストは期待される動作を記録し、将来の変更によるリグレッションを検出します |
| 「テストは後で追加します」 | テストは仕様です。テストがなければコードの動作は未定義で、結局後回しのままになります |
| 「すべてをモックすればテストは速くなります」 | 過剰にモックしたテストは、実際のシステムが失敗しても通過します。呼び出しチェーンの奥深くではなく、境界でモックしてください |
| 「カバレッジ 100%ならコードは正しいです」 | カバレッジが測定するのは実行であり、正しさではありません。意味のあるアサーションなしにコードを実行するだけのテストには価値がありません |
| 「正常系のテストだけで十分です」 | 本番環境でのインシデントのほとんどは、エッジケースやエラーパスが原因です。正常系だけのテストは誤った安心感を与えます |
危険信号
- エッジケースやエラーパスがなく、正常系だけを対象とするテスト
- 動作(「見つからない場合に 404 を返す」)ではなく、実装(「test_calls_function」)を説明するテスト名
- 1 つのテストで 2 つを超えるモックを使用していること。テスト対象のユニットの結合度が高すぎることを示しています
- 実行順序や共有された変更可能な状態に依存するテスト
- 観測可能な動作ではなく、実装の詳細(モックの呼び出し回数)に対するアサーション
- 「ユニットテストでカバーできる」という理由で結合テストを省略すること
検証
- テストが Arrange-Act-Assert 構造に従い、各フェーズが明確に分かれている
- テスト名が動作を説明している:
test_<unit>_<scenario>_<expected_outcome> - エッジケースを網羅している:空の入力、境界値、エラーパス、null/None
- カバレッジがしきい値を満たしている:全体で 80%、新規コードで 90%、重要なパスで 95%
- すべてのテストが成功する:
pytest/npm testが、出力を取得した状態で終了コード 0 を返す - 実行順序に依存するテストがなく、任意の順序で実行できる
- モックは境界(外部 API、システムクロック、ユニットテストにおけるファイルシステム)でのみ使用している