mcpField
C
characterisation-tests

characterisation-tests

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

characterisation-tests について

既存コードの実際の振る舞いを一時的な特性テストに記録するスキルです。仕様が不足したレガシーコードの安全なリファクタリングを支援し、後に振る舞い駆動テストへ置き換えます。

主な機能

  1. 01正しさではなく観測した振る舞いを記録
  2. 02失敗するプレースホルダーで実際の出力を確認
  3. 03一時テストの命名とファイル規則を提示
  4. 04非同期処理、時間制御、長い出力に対応
  5. 05テストの置き換えと削除時期を定義

活用シーン

  1. 01十分なテストがないレガシーコードのリファクタリング
  2. 02仕様が不足したコードの振る舞い調査
  3. 03関数の変更前に出力を記録
  4. 04時間を制御して非同期経路を特性化

インストール

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

$npx skills add https://github.com/citypaul/.dotfiles --skill characterisation-tests

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

同梱ファイルresources/modern-tooling.mdresources/writing-process.md

詳細

特性把握テスト

テストできないコードをまずテスト可能にするには、finding-seams スキルを読み込んでください。新しい振る舞いをテスト駆動で開発するには、tdd スキルを読み込んでください。一般的なテストパターンについては、testing スキルを読み込んでください。特性を把握する際は、ミューテーションを考慮したテスト設計ルールを使用してください。リポジトリでミューテーションテストが必須である場合、または変更リスクを踏まえて追加が妥当な場合は、リポジトリで選択されている検証時点で、蓄積された変更に対してmutation-testingを読み込んでください。

詳細な資料はresources/ディレクトリにあります。必要に応じて読み込んでください。

資料読み込むタイミング
writing-process.md対象を絞ったテスト、非同期コード、いつ停止するかの指針を含む、特性把握プロセス全体の具体例が必要な場合
modern-tooling.mdテスト対象のコードが複数行のテキスト、レンダリングされたドキュメント、大きなオブジェクトを返す場合(スナップショットテストや承認テスト)、多数の入力の組み合わせを試したい場合、または振る舞いが非決定的な場合

核となる概念

特性把握テストとは、コードの一部が実際にどのように振る舞うかを記録するテストです。「こう振る舞うべき」という前提はなく、システムが実際に何をするかを記録します。

特性把握テストに正しさを判断する権限はありません。正しさを検証するのではなく、変更を検出します。特性把握テストが失敗したとき、その変更が意図されたものかどうかは、人が判断します。ゴールデンマスターテストや承認テストとも呼ばれます。呼び名は異なりますが、概念は同じです。

— Michael Feathers、Working Effectively with Legacy Code(2004)

使用する場面

  • テストのない(またはテストが不十分な)既存コードを変更する場合
  • 仕様が存在しない、不完全である、または実行中のシステムと矛盾している場合
  • コードが複雑すぎて、読むだけでは振る舞いを把握できない場合
  • レガシーコードのジレンマに直面している場合:安全にリファクタリングするにはテストが必要だが、コードがテストしにくい
  • 変更前に、関数が実際に何を返すかを把握する必要がある場合

使用しない場面

  • グリーンフィールドのコード — 新しいコードは最初からテスト駆動で開発してください(tdd スキルを参照)
  • すでに仕様がある場合 — 要件が明確でコードが新しいなら、コードが何をするかを記録する特性把握テストではなく、意図した振る舞いを検証する振る舞い駆動テストを書いてください
  • コードにすでに十分なテストがある場合 — テストされていない箇所だけを特性把握し、既存のカバレッジを重複させないでください
  • 恒久的なテスト戦略として — 特性把握テストは足場です。リファクタリングに合わせて、適切なテストに置き換えてください

命名と識別

特性把握テストは、他のLLMにも、人にも、将来の自分にも、ひと目で特性把握テストだと分かるようにしてください。テストファイルを読む人が、これらのテストは実際の振る舞いを記録するものであり、正しさを検証するものではなく、一時的なものであることをすぐに理解できる必要があります。

テストの命名

振る舞い駆動テストと区別するため、テスト名にcharacterisesを使用してください。

// ✅ Clearly identified as characterisation tests
describe('calculateBonus characterisation', () => {
  it('characterises premium-member bonus for < 5 years', () => { ... });
  it('characterises business-member loyalty threshold', () => { ... });
});

// ❌ Indistinguishable from behavior-driven tests
describe('calculateBonus', () => {
  it('should apply a premium-member bonus', () => { ... });
});
ファイルの命名

特性把握テストをファイルツリー上で視覚的に区別できるよう、専用のファイル接尾辞を使用してください。

scoring.characterisation.test.ts    ← characterisation tests (temporary)
member-bonus.test.ts                ← behavior-driven tests (permanent)

一時的な特性把握ファイルは、実装ファイルと1対1で対応させてもかまいません。これは、そのファイルの現在の振る舞いを固定し、後で削除するためのものです。恒久的なテストはファイルの境界ではなく、振る舞いに沿って作成してください(testing スキルの「1対1対応なし」ルールを参照)。

テスト内のドキュメント

各特性把握テストファイルの先頭に、目的と予定しているライフサイクルを説明するブロックコメントを追加してください。コメントが不可欠となる数少ない場面のひとつです。テスト自体はコードが何をするかを記録しますが、コメントはなぜこれらのテストが存在し、いつ削除するかを説明します。

/**
 * CHARACTERISATION TESTS -- documenting actual behavior, NOT asserting correctness.
 *
 * These tests pin down the current behavior of calculateBonus so we can
 * safely refactor it. They should be replaced with behavior-driven tests
 * as the code is understood and restructured.
 *
 * See: characterisation-tests skill for the methodology.
 */
describe('calculateBonus characterisation', () => { ... });
疑わしい振る舞い

バグのように見える振る舞いを特性把握テストで記録する場合は、それが明確に分かるようにしてください。

it('characterises negative activity handling -- SUSPICIOUS: returns negative bonus', () => {
  // This may be a bug -- negative activity produces a negative bonus.
  // Documented as-is; escalate before changing.
  expect(calculateBonus(-5, 'premium', 3)).toBe(-0.75);
});

アルゴリズム

  1. テストハーネスでコードを使用します
  2. 失敗するとわかっているアサーションを記述します("PLACEHOLDER"のようなダミー値を使用します)
  3. テストを実行し、失敗から動作を確認します -- テストランナーが実際の値を表示します
  4. コードが実際に生成する動作を期待するよう、テストを変更します
  5. これを繰り返します -- 好奇心に従って進めます。コード自体が、次に何をテストすべきかを示してくれます

実行して確認していない期待値を入力してはいけません。 すべての期待値は、実際のモジュールを実行して得られます。失敗するプレースホルダーアサーション、記録されたスナップショット、またはモジュールをインポートする一時的なテストなどです。コードを読んで答えを導き出したり、関数を使い捨てスクリプトにコピーしてそのコピーを実行したりするのは、特性の記録ではありません。そうすると、コードに対する自分の読み方を固定してしまい、読み違いがあれば、その誤りを証明するテストになってしまいます。プレースホルダーを記述して実行し、ランナーが出力した内容を貼り付けます。テストファイルは少なくとも2回書き換え、その間に実行を挟みます。

オラクルの発見は、特性テスト用ファイル/名前、または対象を絞った単発の実行に集中させます。再利用するウォッチャーには、tdd スキルの標準的なシード、ライフサイクル、クリーンアップ、証明ポリシーを適用します。クリーンベースラインからの --changed ウォッチャーが実装グラフを読み込んでいると想定してはいけません。オラクルが安定したら、モノレポのルートから、リポジトリが管理する、またはランナーから導出された影響範囲を使い、推移的な利用側も対象として残るようにします。PR の準備が整った段階でウォッチャーを停止し、対象リポジトリのミューテーションポリシーと、設定済みのすべてのプロジェクトに対する完全な非ウォッチゲートを適用します。

// Step 2: write assertion you know will fail
it('characterises formatPrice', () => {
  expect(formatPrice(1999)).toBe('PLACEHOLDER');
});
// Test output: expected 'PLACEHOLDER' but received '$19.99'

// Step 4: change test to expect actual behavior
it('characterises formatPrice', () => {
  expect(formatPrice(1999)).toBe('$19.99');
});

1行より長い出力 -- レンダリングされたテキスト、整形済みドキュメント、大きなオブジェクトなど -- は、手入力の文字列ではなくインラインスナップショットで固定します。 引数なしで toMatchInlineSnapshot() を呼び出して1回実行すると、ランナーが観測した出力をテストファイルに書き込みます。上記のアルゴリズムを自動化したもので、1文字でも間違っていれば失敗します。ひと目で確認できるほど短い場合に限り、期待値を手入力します。複数行の出力を固定する前に modern-tooling.md を読み、インラインスナップショットとファイルスナップショットの選択、組み合わせの網羅的テスト、非決定性を確認してください。

ヒューリスティクス

  1. 対象を絞ったテスト範囲を指針にします -- まず変更箇所を対象にします(たとえば Vitest の変更/関連カバレッジや、担当するテストプロジェクトなど)。その後、必要なカバレッジゲートをすべて実行し、最終的なリポジトリ全体の PR 前テスト実行に進む前に修正を完了します
  2. 本番環境での動作は互換性の証拠であり、正しさの基準ではありません -- 利用側、到達可能性、セキュリティ、受け入れ要件の証拠によって維持すべきかどうかが明らかになるまでは、デプロイ済み環境での観測結果を互換性に関わるものとして扱います
  3. 変更箇所に集中します -- コードベース全体の特性を記録する必要はありません。これから変更するコードだけを対象にします
  4. 疑わしい動作には印を付けます -- バグのように見えるものを見つけたら、テストに記録して疑わしい動作として明示します。黙って「修正」してはいけません
  5. コードを確認します -- これらはブラックボックステストではありません。特性を記録する経路を決めるため、コードを読みます
  6. 変更に見合ったテスト強度の証拠を計画します -- 特性を記録する際、明らかな不足を低コストで強化するためにミューテータールールを使います。リポジトリで自動ミューテーション実行が選ばれている場合は、蓄積された変更が選択した検証ポイントに達するまで、ハーネスの導入を待ちます。カバレッジからわかるのはどの経路が実行されたかです。ミューテーションテストでは、どの経路が保護されているかを確認できます。

いつやめるか

コードベース全体を100%網羅する必要はありません。次の条件を満たしたら、作業を終えてください。

  • これから行う変更が触れるすべての分岐を、特性テストで実行している
  • 変更箇所の1層外側もカバーしている(変更するコードを呼び出す分岐、または変更するコードから呼び出される分岐)
  • 変更する経路における起こりそうなミューテーションのリスクが特性テストの例に反映されている。自動ミューテーションチェックを実行するのは、リポジトリのポリシーで求められている場合、または変更のリスクに応じて必要な場合だけです

これから変更しようとしているコードの誤りをテストで検出できると確信できない場合は、テストを追加してください。確信できるなら、そこで終わりです。

バグを見つけた場合

レガシーコードにはすべてバグがあります。特性テスト中にバグを見つけた場合は、次のように対応してください。

  • システムがデプロイ済みの場合:「バグのある」挙動に依存している人がいるかもしれません。その挙動を記録し、テストに注意が必要な旨を示して、互換性についての判断が下されるまで維持し、変更する前にエスカレーションしてください。
  • システムがまだデプロイされていない場合:互換性のリスクは低くなりますが、未デプロイだからといって正しいとは限りません。まず信頼できる要件、またはユーザーの判断に基づいて、意図された挙動を確認してください。その後、観測された不具合を記録して修正し、古い挙動を特性化するテストを、意図された挙動の回帰テストに置き換えてください。
  • 失敗し続けるアサーションではなく、証拠を残してください:古い挙動を対象とするテストを維持するのは、その挙動を意図的に保つ間だけです。

特性テストは一時的なものです

特性テストはリファクタリングを可能にし、その後、適切な振る舞い駆動型テストに置き換えられます。

  1. 特性化 -- 現在の挙動を安全策として固定する
  2. リファクタリング -- 特性テストで挙動の変化を検出しながら、コードを再構成する
  3. 置き換え -- コードを理解するにつれて、意図された挙動を検証する適切なテストを書く
  4. 削除 -- 適切なテストで同じ挙動をカバーできたら、特性テストを終了する

森に入って、「このエリアはすべて自分のものだ」と線を引くようなものです。それが済めば、リファクタリングやテストの追加を通じて開発を進められます。特性テストは、時間がたてば削除できます。

非同期コードの特性化

レガシーな非同期コードにも同じアルゴリズムを適用します。主な違いは、結果を待機し、タイミングを制御することです。

// Step 1: dummy assertion, same algorithm
it('characterises fetchUserOrders', async () => {
  const result = await fetchUserOrders('user-123');
  expect(result).toBe('PLACEHOLDER');
});
// Output: expected 'PLACEHOLDER' but received [{ id: 'order-1', ... }]

// Step 2: record actual behavior
it('characterises fetchUserOrders for known user', async () => {
  const result = await fetchUserOrders('user-123');
  expect(result).toEqual([
    expect.objectContaining({ id: 'order-1', status: 'shipped' }),
  ]);
});

非同期コードの特性化で重要な点:

  • I/O には実際のシームを使う -- 実際のサービスにアクセスするのではなく、非同期の依存関係をパラメーターとして渡します(finding-seams スキルを参照)
  • エラー経路 -- 解決済み状態と拒否状態の両方を特性化します:await expect(fn()).rejects.toThrow()
  • タイミングに依存する挙動 -- vi.useFakeTimers() と vi.advanceTimersByTime() を使って時間を制御します(modern-tooling.md を参照)
  • ストリームとイベント -- 発行された値を配列に集めてから、収集した結果をアサートします
// Characterising an event emitter
it('characterises order processor events', async () => {
  const events: string[] = [];
  processor.on('status', (s: string) => events.push(s));
  await processor.process(testOrder);
  expect(events).toEqual(['validating', 'processing', 'complete']);
});

よくある間違い

間違い修正方法
特性テストを恒久的なものとして扱うこれらは足場です。リファクタリングに合わせて、振る舞い駆動テストに置き換えてください
特性テストのバグを「修正」する実際の振る舞いを記録し、疑わしいものとしてマークして、エスカレーションしてください
コードベース全体を特性化しようとするこれから変更する領域と、その外側の1層に集中してください
コードが どう振る舞うべきか に基づいて特性テストを書くコードが実際にどう振る舞うかを明らかにしてください。上記のアルゴリズムを使います
コードを読んで、またはスクラッチスクリプト内の関数のコピーから期待値を計算する実際のモジュールを実行し、すべての期待値を実行結果または記録されたスナップショットから取得してください
カバレッジをテストの強さの証拠として扱う起こりそうなミュータントのリスクに対するアサーションを追加してください。リポジトリの方針や変更リスクに応じて、累積スコープのミューテーション実行を使用してください
新しいコードに特性テストを使う新しいコードはテスト駆動で開発してください(tdd スキルを参照)
センシングのためにパラメーター注入ではなく vi.mock() を使うセンシング関数をパラメーターとして渡してください(finding-seams スキルを参照)
非同期の結果を await しない特性テストでは async/await を使ってください。Promise に対する同期的なアサーションは必ず成功します

コメント