エージェントのテストは信用できない
原文は Alex O'Callaghan により に公開されました。 このブログを購読する
エージェントを使って53件のEnzymeテストスイートをReact Testing Libraryへ移行した。テストは通り、コードも一見まとまって見えた。しかしよく見てみると、リグレッションを検出できないテスト、間違った対象を検証しているテスト、そもそも何もテストしていないに等しいものが見つかった。
エージェントが生成したテストでは、こうした問題は見逃しやすい。出力は洗練されていてコードもきれい、しかも量が多いからだ。
EnzymeからRTLへの移行
Enzymeのテストは、長年バックログに積まれたまま優先度の低い技術的負債だった。まさにこうした明確で機械的な作業こそ、エージェントが得意とするはずの領域だ。複雑さごとに分割し、自作のprepare-mr-skillでコミットを整理して説明文を書かせたところ、うまくいった。少なくとも、うまくいったように見えた。テストは移行され、パスし、コードの変更も妥当に見えたのだ。
マージする前に、いつものチームレビューのプロセスでMRを確認したところ、一見正しく見えてもリグレッションを検出できないテストが見つかった。例えば、特定のARIAロールを持つ要素がレンダリングされていないことを検証するテストがあったが、そのロール自体が無効で、そもそもそのコンポーネントがその要素をレンダリングすることはあり得なかった。またdisabledとaria-disabledの混同もあった。両者は挙動が異なりアクセシビリティ上も重要だが、見た目は似ているため、一見もっともらしいテストが間違っていても失敗せずに通ってしまうのだ。
なぜこうなるのか
この問題はエージェント特有のものではない。手書きでもこうしたテストは簡単に書けてしまう。ただし、間違ったアサーションを書いた人間であれば、コンポーネントを理解している分「このテスト、簡単すぎないか」と気づくことがある。エージェントに直感はない。実装からパターンマッチしてそれらしいものを作っては次へ進むだけだ。しかもエージェントが生成するコードの量の多さが、こうした問題を見逃させやすくする。
実装がすでに存在する場合(例えばテストの移行、カバレッジの補填、何年も前に書かれたテストの検証など)、テスト駆動開発(TDD)という本来の安全装置は機能しない。動いているコードに対してテストを書くため、間違ったアサーションが失敗する機会がないのだ。これを検出する唯一の方法は、意図的にコンポーネントを壊してみることだ。一時的にコンポーネントを変更し、テストがそれを検出するか確認してから元に戻す。
検証を確実に実行させるには
最初に思いついたのは、プロンプトを修正してエージェントにこうしたミスをチェックさせることだった。RTL移行用のスキルを更新し、すべてのテストを失敗する状態で検証することを明示的に求めるようにした。
しかし安定しては機能しなかった。エージェントは要件を一応考慮し、時々何かを見つけはするが、すぐに次へ進んでしまう。検証を制約としてではなく、チェックボックスのひとつとして扱っていたのだ。
問題の一部は構造にある。大量のテストを一括で移行させようとすると、タスクは長くなりコンテキストが埋まっていく。コンテキストが増えるにつれ、モデルは指示へのフォーカスを失い始める。コンテキストウィンドウの上限に近づくと、作業を早めに切り上げようとする「コンテキスト不安」を示すモデルもある。こうしてエージェントは、検証のような指示を忘れたりスキップしたりするようになる。
一括ではなくテストスイートごとに移行を実行してみた。コンテキストウィンドウを短くすれば完了へのプレッシャーが減り、検証のステップに時間をかける余裕が生まれると考えたからだ。エージェントがチェックする確率は上がったが、すべてのテストではなく「いくつかの主要なテストだけ」を検証すると自己判断することが多かった。
最終的に、移行とレビューのステップを2つの別々のエージェントタスクに分けることにした。各テストスイートをループし、2つのプロンプトを順に実行するbashスクリプトを書いた。1つは移行用、もう1つはレビュー用だ。
#!/bin/bash
echo "Starting migration..."
declare -a files=(
"path/to/test-suite.test.tsx"
)
for i in "${files[@]}"; do
echo "Migrating $i"
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Migrate $i to use RTL. Ensure linting passes and then commit your changes in a single commit."
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Review the RTL tests in $i -
* Identify any unnecessary tests, remove them
* Identify any tests not following best practices, fix them
* For every single test intentionally change the implementation to break the specific feature tested and confirm the test fails as expected, fix them if they don't
Ensure linting passes and then commit your changes in a single commit."
echo "Migrated $i"
done検証に特化したタスクにしたことで、エージェントは自らToDoリストを作成し、各テストを徹底的に処理した。問題を検出しただけでなく、レビューで見つかっていたものを超える改善も行った。
かかったコスト
その方法はうまくいったが、コストは大幅に増えた。すべての移行はCursor上でclaude-4.6-sonnet-mediumを使って22件のテストスイートに対して実行した。
| アプローチ | コスト |
|---|---|
| 一括処理 | ~$13 |
| スイートごと | ~$32 |
| スイートごと+レビュー | ~$62 |
スイートごとのレビューを追加する方式は、人間のレビューで以前見つかっていた問題をすべて解消したが、一括処理と比べるとコストはほぼ5倍になった。
エージェントのおかげで私たちはより多くのことができるようになり、バックログに眠っていたはずのモダナイゼーション作業を完了できる。だが、それを信頼できる結果とともにきちんとやろうとすると、思った以上にコストがかかる。やるならきちんとやるべきだと思うが、たとえ所要時間が大幅に短縮されたとしても、この作業にかかる実際の金銭的コストとのトレードオフは存在する。
組織は、この向上した生産性をどう収益の増加につなげるのかを意図的に考える必要がある。AIベンダーへの請求額が増えるだけに終わらせないためにも。
まとめ
2つのプロンプトを使うスクリプトは、より良いプロンプトだったわけではなく、異なるアーキテクチャだったのだ。1つのエージェントに移行と検証を同じパスでやらせると、タスク構造自体が検証の妨げになる。プロセスのステップが一貫して抜け落ちる場合、解決策はたいていより良い指示ではない。そのステップをスキップできないワークフローこそが必要なのだ。
ここでレビュアーが概念的にやっていることは、ミューテーションテストだ。アサーションごとに、コンポーネントを変更したら失敗するかどうかを問うている。Strykerのようなミューテーションテストツールをテストパイプラインに組み込めば、こうした問題の検出に役立ち、エージェントに手作業でやらせるコストを削減できるかもしれない。
エージェントのワークフローを設計する時間も作業の一部であり、エージェントによる生産性向上を評価する際には考慮すべきだ。それが、信頼できるアウトプットと、信頼できそうに見えるだけのアウトプットとの違いを分けるのだ。
記事をランダムに読む
コメント
ログインしてコメントする