mcpField
A
awesome-test-writing

awesome-test-writing

github.com/khaskyテスト・品質2283 回インストール1 週間前に更新
リポジトリ

awesome-test-writing について

awesome-test-writingは、開発者向けのテスト作成スキルです。既存の規約に沿い、現実的な回帰を検出できることを失敗・成功の両方で確かめます。

主な機能

  1. 01既存のテスト規約に合わせる
  2. 02不具合リスクに応じてテスト層を選ぶ
  3. 03公開インターフェースを事前に確認
  4. 04実装前に振る舞いのケースを設計
  5. 05テストの失敗と成功を検証
  6. 06対象を絞ったミューテーションテスト

活用シーン

  1. 01バグ修正の回帰テストを追加
  2. 02レガシーコードのリファクタリングに備える
  3. 03ファジングやプロパティテストで入力解析を検査

インストール

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

$npx skills add https://github.com/khasky/awesome-agent-skills --skill awesome-test-writing

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

同梱ファイルevals/eval_queries.jsonreferences/fuzzing.md

詳細

テストの作成

動作が壊れたときに失敗し、リファクタリングを通じて正常な状態を保つテストを書きます。成果物はカバレッジではなく、異常を検知する仕組みです。テスト対象のコードが現実的な形で退行するたびに、少なくとも1つのテストが失敗するようにします。壊れたコードに対して失敗しないテストは、テストのふりをしたドキュメントです。

いつ有効にするか

  • 「Xのテストを書いて」「カバレッジを追加して」「このモジュールにはテストがない」。
  • バグ修正に回帰テストが必要な場合(awesome-bug-fixから引き継ぎます)。
  • レビューや監査の指摘で「テストが必要」とされている場合(awesome-code-review、awesome-security-audit、awesome-dependency-audit)。
  • リファクタリングの前に、レガシーコードを保護する仕組みが必要な場合(特性化テスト)。

差分のテスト品質をレビューする場合(awesome-code-reviewのフェーズ4が担当します)や、失敗の根本原因を特定する場合(awesome-bug-fixが担当します)には有効にしないでください。

作業手順

  1. まず既存の構成を確認します。ランナー、アサーションのスタイル、フィクスチャ/ファクトリーの慣例、ファイルの配置、命名パターンを調べます。新しいテストはリポジトリの既存の形式に合わせ、別のテストフレームワークや並行する慣例は導入しません。ランナーがまったくない場合は、エコシステムの標準を提案して承認を待ちます(新しい依存関係となるためです)。
  2. テスト対象のコードを最初から最後まで読みます。公開シーム、入力、出力、副作用、エラーパスを確認します。テストに使うシームは、呼び出し元が実際に使用するものでなければなりません。テストのために非公開関数をエクスポートする必要があるなら、それは回避するのではなく、報告すべき設計上の兆候です。
  3. 不具合を検出できる最も低いレベルに配置を決めます。純粋なロジックにはユニットテスト、配線、クエリ、コントラクトにはインテグレーションテストを使い、E2E は重要なユーザーフローに限ります。ユニットテストで検出できる不具合を E2E スイートだけで検出すると、その実行のたびにコストが 100 倍かかり続けます。
  4. テストコードを書く前にシームについて合意します。テストが対象とするシーム(公開エントリーポイント、観測する境界)を書き出し、ユーザーに提示します。確認されていないシームはテストしません。すべてをテストすることはできません。事前にシームについて合意することで、あらゆるエッジケースに均等に労力を広げるのではなく、クリティカルパスと複雑なロジックに注力できます。また、テストを間違ったレベルで 20 件作ってしまう前に、認識の違いを明らかにできます。インターフェースの形自体が問われている場合(モジュールの深さやシームの位置など)は、awesome-architecture-audit/references/design-vocabulary.md に用語がまとめられています。そのスキルがインストールされていなくても、シームのリストについて合意すれば拘束力は変わらず、共有する用語だけが不足します。
  5. コードを書く前にケースのリストを設計します。正常系、境界値(空、1 件、複数件、最大値)、エラーパス(無効な入力、依存関係の失敗、タイムアウト)、およびそのコードで起こりやすい不具合の種類(ページネーションのオフバイワン、日付計算のタイムゾーン、確認後に実行する処理の競合)を含めます。ガード、catch、フォールバック、正規化が存在するのは、かつて必要とされた入力があるからです。その入力を見つけてケースにします。コードを読めばケースは見つかりますが、答えは見つかりません。期待値はそれぞれコントラクト(ドキュメント、型、呼び出し元が必要とするもの)に基づいて決めます。現在のコードが返す値をそのまま期待値にすると、その不具合がテストの期待値になってしまうためです。各状態または列挙値について、2 つのリストを使って各コンシューマーと照合します。定義で宣言されている値と、実際に処理する分岐のリストを行番号付きで作成します。最初のリストにあって 2 つ目にない値は、テストされていない(多くの場合、未処理の)ケースです。リストを書き出し、各ケースを、動作上の焦点を 1 つに絞った 1 件のテストにします。
  6. 動作を優先して書きます。公開シームを通じて観測可能な結果(戻り値、状態変更、発行イベント、記録された呼び出し)をアサートし、内部実装(非公開フィールド、ヘルパーの呼び出し順序)はアサートしません。テスト名にはシナリオと期待値を記述します(「test token 2」ではなく「期限切れのトークンを 401 で拒否する」)。
  7. すべてのテストが失敗することを確認します。意図的に壊したコード(修正を元に戻す、分岐を反転する、定数を壊す)に対して新しいテストを実行し、失敗を確認してから、元に戻して成功を確認します。回帰テストは、修正なしでは失敗し、修正ありでは成功することを両方確認して初めて検証済みとなります。一度も失敗したことのないテストは、何も証明していません。
  8. スイートを実行して報告します。関連する範囲全体を実行し、終了コードを確認し、不安定な動作を報告します(再実行時にしか成功しないテストは不具合であり、成功ではありません。隔離して報告し、成功するまで黙って再試行してはいけません)。新しいテストについて、成功、失敗、エラー、スキップをそれぞれ分けて報告します。セットアップエラー、フィクスチャの欠落、依存関係の欠落によるスキップは成功ではなく、新しいテストがスキップされた場合は何も証明していません。

分野別のルール

  • フィクスチャとファクトリー — 各テストが自身のデータを所有します。テストごとに上書きできるファクトリーを使い、共有の可変シードコーパスは設けず、他のテストが残したデータに依存しないようにします。統合テストでは、テストごとの分離(トランザクションのロールバック、テストごとのスキーマ、または一意のキー)を確保します。
  • 決定性 — 時刻を固定し、TZ=UTC で実行します。乱数にはシードを設定するか、注入します。sleep は使わず、期限を設けて条件を待ちます。実時間、ネットワークの状態、実行順序に依存するテストは、構造上フレーキーです。
  • モック — プロセスが所有しないもの(外部 HTTP、サードパーティー SDK、決済プロバイダー)のみをモックし、境界内のものにはすべて実際のコードを使います。モックを使う場合は、用意したレスポンスの効果だけでなく、モックに送られたリクエスト(ペイロード、ヘッダー、冪等性キー)も検証します。
  • 既存動作の特性化テスト(レガシーコード) — テストされていないコードをリファクタリングする前に、現在の観測可能な動作をテストで固定します。あるべき動作ではなく、実際の動作(奇妙な部分も含む)を検証します。これがリファクタリングの検証を可能にする安全網となり、動作を意図的に変更したら見直します。
  • プロパティテストとファズテスト — 信頼できないバイト列(ファイル形式、プロトコルフレーム、ユーザーマークアップ)をパースするものには、失敗したコーパスを回帰用シードとしてコミットし、プロパティベーステストまたはファズテストを適用します。例ベースのテストだけでは、攻撃者が試す入力を体系的に見落とします。常設の敵対的入力フィクスチャセット(../../etc/passwd、' OR 1=1--、<img onerror=…>、10MB の文字列、RTL/ゼロ幅 Unicode)を、入力処理のすべての境界で実行します。シリアライズ/パース、またはエンコード/デコードのペアには、生成した入力に対するラウンドトリッププロパティを適用します。空の値、最大値、非 ASCII 値も含めて decode(encode(x)) == x を検証します。これにより、手書きの例を一つも追加せずに、片側が処理し忘れたフィールドを検出できます。ハーネスのルール(決定性、サイズ検証、グローバル状態のリセット)、エコシステムごとのファザー(libFuzzer/AFL++/cargo-fuzz/Atheris/Go)、サニタイザービルド(ASan/UBSan)、シグナルとしてのカバレッジ、wycheproof の暗号ベクトルについては、references/fuzzing.md を参照してください。テスト対象の境界が信頼できないバイト列をパースするときに読み、通常の単体テストでは参照しないでください。
  • red → green のループ — 1 サイクルにつき、1 つの境界、1 つのテスト、1 つの最小限の実装を、垂直スライスで進めます。失敗するテストを書き、失敗することを確認し、通すのに必要なコードだけを書いて、繰り返します。すべてのテストを先に書いてから実装をすべて書くと、テストが検証するのは想像上の動作になります。重要だと判明した動作ではなく、推測した形をテストで固定してしまい、意味のある変更に対して反応しなくなります。リファクタリングはこのループの 3 つ目のステップではありません。コードの再構成はレビュー段階(awesome-code-review、リポジトリ全体を対象とする場合は awesome-code-cleanup)で行います。ここで書いたテストが、それを安全に行うためのものです。
  • カバレッジ — 弱いシグナルであり、目標ではありません。テストするつもりだった未テストの分岐を見つけるために使い、数値を追い求めないでください。鏡合わせのアサーションで 100% のカバレッジを達成しても、防御効果は 0% です。
  • ミューテーションテスト — ステップ 7 をスイート全体に適用したものです。ステップ 7 では、1 つのテストが失敗することを確認します。ミューテーションテストでは、コードが変わったときにスイート全体がそれを検知するかを確認します。ツールが本番コードを書き換え(比較演算子を反転する、境界を入れ替える、文を削除する、定数を返すなど)、どのミュータントをテストが排除できず、生き残らせたかを報告します。生き残ったミュータントは、名前の付いた穴です。つまり、スイートが実行はするものの検証していない行です。対象を絞って実行します。1 つのモジュールや、金銭、認証、パーサーを扱うモジュールが対象です。初回からリポジトリ全体を対象にしないでください。リポジトリ全体のミューテーション実行には数分から数時間の CPU 時間がかかり、誰も読まないレポートが生成されます。エコシステムごとのツール:Stryker(JS/TS、.NET、Scala)、Python 用の mutmut/cosmic-ray、PIT(JVM)、cargo-mutants(Rust)、go-mutesting(Go)。スコアではなく、生き残ったミュータントに対処します。ミューテーション率をそれ自体のために追い求めても、カバレッジ数値と同じ水増しにしかなりません。同値ミュータント(観測可能な動作を変えられない変更)はノイズです。印を付けて先に進みます。
  • その他の言語 — 上記はすべてテストランナーに依存しません。変わるのは仕組みの名前だけです(fake timers と freezegun と注入したクロック、t.Parallel() による分離とトランザクションのロールバックなど)。別のエコシステムの習慣ではなく、リポジトリの既存テストから慣用的な方法を見つけてください。

出力

Tests written — <module / behavior> — <N> tests

Cases covered:
- <scenario> → <expectation>   [unit|integration|e2e]
...

Proven to fail: <which mutation/revert made each go red>
Run: <command> → <N passed, 0 failed, 0 errored, 0 skipped, exit 0>  (new tests counted apart from the existing suite)
Not covered (and why): <cases deferred, with reason — not silence>

アンチパターン

アンチパターン代わりに
ミラーアサーション — コードと同じ方法で期待値を計算する仕様または手計算に基づく、正しいことが分かっている値をハードコードする
変更検出テスト — 再設計のたびに失敗する内部構造のスナップショット外部から観測できる契約を検証し、スナップショットは真の出力面に限る
アサーション・ルーレット — どれが失敗したのか分からない、説明のないアサーションを多数使うテストごとに行動上の焦点を1つに絞り、自明でないアサーションにはメッセージを付ける
モックのテスト — スタブが指示された値を返したことを検証するモックに対するシステムの動作と、モックに渡されたリクエストを検証する
形状だけのアサーション — 値が存在することだけを確認し、その内容を確認しない値そのものを検証する
最初に見つかったものだけの確認 — 多数の中から正しい項目を1つ見つけて終了する件数、順序、誤った項目がないことなど、コレクション全体を検証する
片側だけの否定テスト — 拒否時に、ある結果が起きないことだけを検証する書き込み、イベント、副作用がなく、状態が変わらないなど、あらゆる結果が起きないことを検証する
バリデーターが先に拒否する変異入力や境界入力バリデーションを通過する入力を使い、バリデーターではなくロジックをテストする(バリデーターは別途テストする)
既存のランナーの隣に新しいフレームワークを導入する別のもののほうが優れていても、リポジトリのランナーを使う
カバレッジ目当ての水増し — 単純なゲッターをテストする一方で、エラーパスは手薄にする振る舞いとリスクに基づいてケースを列挙し、まず境界条件と失敗を確認する
修正後に書かれ、一度も失敗を確認していないテスト修正を戻して失敗を確認し、修正を適用し直して成功を確認する — 毎回、両方の方法で

コメント