mcpField
T
test-driven-investigation

test-driven-investigation

github.com/cloudflareテスト・品質9.1k45 回インストール今日更新
リポジトリ

test-driven-investigation について

最小限の再現テストを早い段階で作成・実行し、その結果を手がかりにバグを調査するスキルです。workerd などの開発者向けに、テスト選び、コミット履歴の確認、対象コード経路の検証方法を示します。

主な機能

  1. 01早期に最小再現テストを作成
  2. 02テスト結果を調査の指針にする
  3. 03関連するコミット履歴を確認
  4. 04可能な場合は既存テストを応用
  5. 05対象コード経路の実行を検証

活用シーン

  1. 01クラッシュやアサーション失敗の調査
  2. 02最近の変更に関する仮説の検証
  3. 03範囲を絞ったテストでバグを再現
  4. 04成功したテストが対象機能を実行したか確認

インストール

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

$npx skills add https://github.com/cloudflare/workerd --skill test-driven-investigation

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

詳細

テスト駆動の調査

概要

バグを調査するときは、できるだけ早い段階で再現テストを書きます。実験をせずに分析していると、堂々巡りに陥ります。

基本原則: 失敗する20行のテストは、ソースコードを2時間読むより多くのことを教えてくれます。

アンチパターン

READ code → BUILD mental model → READ more code → REVISE model → READ more code →
SECOND-GUESS model → READ same code again → ... → eventually write a test

生産的に感じますが、実際にはそうではありません。現実に基づかず、コードのパターンを探しているだけです。読み返すたびに明確になるどころか、不確実さが増していきます。

パターン

1. READ the error message / assertion / crash
2. IDENTIFY the minimal trigger (what operation, on what object, in what state?)
3. WRITE a test that sets up that state and performs that operation
4. RUN it
5. Let the RESULT guide the next step
6. Iterate as needed, but always grounded in test results backed by code, not just code.

手順1〜2にかける時間は、数分で十分です。テストを書くために、呼び出しチェーン全体を理解する必要はありません。必要なのは、エントリーポイントと何が問題なのかを把握することです。

事前確認

テストを書く前に、確認すべきことが3つあります。どのオブジェクト/APIを使うか、どのテストファイルに追加するか、どうビルドして実行するかです。これらを見つけるために、ツール呼び出しは最大20〜30回までにします。これはテストを書くための範囲を定めた調査であり、際限のないコード分析ではありません。正確なAPIがわからない場合は、見つけた中で最も近いものを選び、とにかくテストを書きます。誤ったAPIを使ったテストが成功したとしても、それは何らかの手がかりになります。

コミット履歴を確認する

調査の早い段階で、関連コードに変更を加えた最近のコミット履歴を確認します。最近発生したバグは、最近導入された可能性が高いです。git log --oneline -20 -- path/to/relevant/filesを実行すれば数秒で確認でき、「どこかで何かがおかしい」という状態から「この特定の変更が原因かもしれない」というところまで、すぐに調査範囲を絞り込めます。

これはテストを書く代わりではなく、より適切な仮説を素早く立てるための方法です。原因の疑いがあるコミットを特定できた場合は、最初のテストでその変更が原因かどうかを確認します。

原因の疑いがあるコミットを見つけた場合: そこで調査を止めてはいけません。必ず、そのコミットの前後それぞれ少なくとも10件のコミットを確認します。バグは単一のコミットではなく、複数の変更が相互に作用して導入されることがよくあります。単独では問題なさそうなコミットが、近くのコミットで前提としていた条件を壊していることもあります。周辺のコミットを確認すれば、確証バイアスも防げます。隣接する変更に、より適切な説明が見つかるかもしれません。

相関関係を因果関係と取り違えないでください。 バグが発生した時期と重なるコミットは手がかりであり、結論ではありません。偶然の可能性もあります。実際の原因は、環境の変更、依存関係の更新、新しい負荷パターンによって発生しやすくなった競合状態、あるいは無関係な変更によって表面化した潜在バグかもしれません。原因の疑いがあるコミットは、罪の証拠ではなく、検証すべき仮説として扱います。そのコミットがバグを引き起こす仕組みを示せないなら、原因を見つけたことにはなりません。

確認するポイント:

  • クラッシュまたはアサーションが発生するファイル/関数への変更
  • 影響を受けるサブシステムのセットアップ、設定、初期化コードへの変更
  • 影響を受けるコードが使用する共通ユーティリティや基底クラスへの変更
  • 該当領域の移動や名前変更を伴うリファクタリング

範囲を限定してください: git logとgit showを使ったツール呼び出しは5回程度にとどめ、考古学調査のように掘り下げないでください。目立ったものがなければ、わかっていることをもとにテストを書き、次に進みます。コミット履歴は仮説を立てるための情報の1つであり、事前に確認しておかなければならないものではありません。

これ以上コードを読みたくなったとき

自分に問いかけてみましょう。

  • 「テストを書くために読んでいるのか、それともテストを書くのを避けるために読んでいるのか?」 テストがどのようなものになるか説明できないなら、解決すべき問題はそこです。さらに読み進めることではありません。
  • 「検証できる仮説はあるか?」 あるなら検証しましょう。ないなら、可能な限りシンプルな仮説を立てて検証しましょう。
  • 「仮説が複数あるか?」 どれかを選んで検証するか、複数を並行して進めましょう。「何かをテストする前にすべてを理解しなければ」と行き詰まったり、頭の中で複数のメンタルモデルをやりくりしようとしたりしないでください。
  • 「すでに読んだコードを読み直しているか?」 そこで止めましょう。行き詰まっています。たとえ間違っていても、今の理解でテストを書きましょう。実行できる間違ったテストからは何かを学べます。検証されない正しいメンタルモデルからは何も学べません。
  • 「同じ経路を何度もたどっているか?」 同じ呼び出しスタックを何度も追っていることに気づいたら、そこで止めましょう。失敗する箇所を直接通るテストを書きましょう。後からいつでも調整できます。必要であれば、すでに読んだ箇所を記録しておくために一時的なトラッキング用ドキュメントを使い、すべてを頭の中に留めたり、同じコードを読み直したりせずに済むようにしましょう。ただし、そのドキュメントを優先してテストの作成と実行を後回しにしてはいけません。

既存のテストから始める

テストを一から書く前に、同じ機能やサブシステムに対する既存のテストを探しましょう。調査中の API、プロトコル、コードパスを実行するテストを検索してください。クラッシュ箇所と同じファイルにあるテストだけを探すのではありません。

既存のテストには、ソースコードを読むだけでは得られない暗黙の知識が盛り込まれています。必要なセットアップ、フレームワーク固有の検証パターン、設定上の癖、シャットダウン処理などです。構造に問題のあるテスト(検証がない、設定が間違っているなど)は、何も実行していないのに気づかれずに「成功」することがあります。

新しく考案するのではなく、動作する既存のテストを調整しましょう。 既存のテストがその機能で動作し、バグがフラグ、自動ゲート、または設定変更によって制御されているなら、動作するテストを複製して変数を1つだけ変更するのが、再現への最短経路となることがよくあります。

テストがコードパスを実行していることを確認する

テストが成功しても、機能が正しく動作した証拠にはなりません。機能がまったく実行されなかった可能性もあります。

テストを実行するたびに、対象のコードパスが実行された証拠が出力にあるか確認しましょう。機能に言及するログ行、実行されたサブリクエスト、想定されるエラーメッセージ、記録されたメトリクスなどです。テスト出力に証拠が見つからない場合、終了ステータスにかかわらず、そのテストは有効ではありません。

具体的な確認項目:

  • テスト出力に、機能のスクリプト、ワーカー、パイプラインの名前が出ていますか?
  • 想定されるサブリクエストや RPC 呼び出しがログに表示されていますか?
  • 機能が副作用(トレースの送信、ストレージへの書き込み、ネットワーク呼び出しなど)を発生させる場合、その副作用を確認できますか?
  • 機能の設定を完全に削除しても、テストは成功しますか? そうなら、そのテストは機能を検証していません。

テストの範囲を決める

本番環境の状況を正確に再現する必要はありません。再現すべきなのは、_仕組み_です。

本番環境でのクラッシュ: KJ_REQUIRE(!writeInProgress, "concurrent write()s not allowed")

必要ないこと: この状態につながる可能性のある、本番環境の呼び出しスタックのあらゆる詳細。

必要なこと: 仮定上のケースに限られるとしても、発生させる最短の経路を見つけること。

Good test scope:
  Create the pipe/adapter/object directly
  → Put it in the suspect state
  → Perform the operation that should fail
  → Assert what happens

Bad test scope:
  Reproduce the entire production call stack
  with all middleware and wrappers

仮説を立てる

バグ調査における仮説とは、次のようなものです。

「Y が失敗した後に X を実行すると、Z が起こる。Y のエラー処理でのクリーンアップが W を行わないためだ。」

次のようなものである必要はありません。

「すべてのコードパスを追跡し、847行目が根本原因だと確信しています。なぜなら、次のような相互作用があるからです……」

最初の形なら数分でテストできます。2つ目は組み立てるのに何時間もかかり、それでも間違っている可能性があります。

テスト後

  • テストが想定どおり失敗する場合: バグの発生メカニズムを確認できました。これで、バグを探すためではなく、修正方法を見つけるという_目的を持って_コードを読めます。
  • テストが成功する場合(バグが再現しない): 仮説が間違っていました。それは有益な結果です。内容を見直して、もう一度試しましょう。さらに1時間コードを読むよりも早く進められます。
  • テストが別の形でクラッシュする場合: 別の問題を見つけました。その調査を進めつつ、元の作業も続けましょう。複数の調査を並行して進めることはできますが、それぞれを単なるコードの読み込みではなく、テスト結果に基づいたものにしてください。

危険信号 — 分析しすぎています

  • 同じファイルや関数を2回以上読んでいます
  • 「何が何を呼び出すか」について、何段階にもわたるメンタルモデルを組み立てています
  • テストコードを書く前に、コードの流れについて詳細なメモを書いています
  • 「テストを書く前に、まず X を理解させて」と何度も言っています
  • 1つのコンポーネントをテストする前に、システム全体を理解する必要があると感じています
  • テストが「成功」しているのに、機能が実行された証拠がありません。テストを修正する代わりに、なぜ成功したのかを説明するためにコードを読んでいます
  • バグを再現しないテストを複数書き、どれか1つでも正しいコードパスを実行しているのか疑問に思っていません

ビルド時間が優先順位を変えるわけではありません

このコードベースでは、C++ のコンパイルとテストに数分かかることがあります。だからといって、テストを後回しにしてコードをさらに読むのは正当化されません。「素早いフィードバック」の意味が変わるだけです。

  • コンパイルが速いコードベース: テストを書いて実行し、数秒で結果を確認して、素早く繰り返します。
  • コンパイルが遅いコードベース: テストを書いてビルドを開始し、待ち時間を次の反復に役立つ的を絞ったコードの読み込みに使います。ビルドは実行中です。作業が止まっているのではなく、並行して進めているのです。

ビルドが遅いと、「コンパイルする前に、もっと確信を持つべきだ」という誘惑に駆られます。これは、分析のループに陥っている状態を隠したものです。最初の試行でバグを再現できなかったとしても、コンパイルして実行できたテストは無駄ではありません。動作確認済みのテストハーネスができたので、次のサイクルで調整できます。多くの場合、増分ビルドはかなり高速です。

このコードベースでの実践

workerd とその依存関係(KJ、Cap'n Proto)には、充実したテスト基盤があります。

  • KJ のテスト: *-test.c++ ファイル内の KJ_TEST("description") { ... }
  • workerd のテスト: JS/TS 統合テスト用の .wd-test 形式
  • ビルドと実行: bazel test //path:target --test_arg='-f...' --test_output=all

ほとんどの KJ/capnp のバグは、公開 API(パイプ、ストリーム、プロミス、HTTP)を使った自己完結型の KJ_TEST で再現できます。内部へのアクセスが必要になることはほとんどありません。

要点

テストを書きます。テストを実行します。結果を分析します。考えます。繰り返します。

逆ではありません。

コメント