nun_game0

AIにテストコードを書かせたら品質が上がった話

AIテストコード

テストを書くのが嫌いだった。機能の実装は楽しいけど、テストは地味で面倒。後回しにした結果、テストカバレッジが低いまま本番に出して、後から痛い目を見るパターンを何度も繰り返していた。

ところがAIにテストコードの生成を任せるようになってから、開発フローが根本的に変わった。この記事では、AIを使ったテスト自動生成の具体的なワークフロー、プロンプトの工夫、品質管理のやり方、そして実際にカバレッジがどう改善したかを記録する。

テストを書かない言い訳と、その代償

「時間がない」「仕様が変わるかもしれない」「動いてるから大丈夫」。テストを書かない理由はいくらでも出てくる。

インフラエンジニアとして、他人のコードにはテストを求めるくせに、自分のスクリプトにはテストを書かない。この矛盾に気づいてはいたけど、習慣を変えるのは難しかった。

テストがないコードの何が辛いかというと、変更するたびに手動で確認する必要があること。本番環境でしか再現しないバグが出て深夜に対応したり、リファクタリングしたくても怖くて触れなかったり。テストがないことのコストは、書かない時には見えなくて、壊れた時に一気に押し寄せてくる。

自分の場合、Goで書いたCLIツールやTerraformのヘルパースクリプトがテストゼロで運用されていた。「自分しか使わないから」と思っていたけど、半年後の自分は完全に他人だった。動作仕様がわからなくて、結局コードを全部読み直す羽目になった。

AIにテストコードを丸投げしてみた

Claude Codeに「この関数のユニットテストを書いて」と投げてみた。正常系、異常系、エッジケースまで含めたテストが数秒で出てきた。

自分で書いたら30分かかる量が、AIなら30秒。しかもエッジケースの網羅性が自分より高い。「空文字を渡した時」「undefinedを渡した時」「配列が空の時」みたいなケースは、自分だと忘れがち。

最初は半信半疑だったけど、生成されたテストを実行したら、実装のバグが2つ見つかった。境界値の処理が甘かったのと、nilチェックが漏れていた箇所。テストを書くつもりで始めたのに、テストがバグを見つけてくれた。

AIが生成したテストケース

テスト生成のワークフロー

自分が落ち着いたワークフローはこんな感じ。

ステップ1: 対象の関数やメソッドを特定する。新規実装なら、まずインターフェース(関数のシグネチャと期待する振る舞い)だけ定義する。既存コードなら、テストがない関数をカバレッジレポートから見つける。

ステップ2: AIにテストを生成させる。単に「テストを書いて」ではなく、コンテキストを渡す。対象の関数コード、依存する型定義、既存のテストファイルがあればそのスタイルも含める。AIは周辺のコードスタイルに合わせてテストを書いてくれるので、プロジェクト内の一貫性が保たれる。

ステップ3: 生成されたテストをレビューする。ここが一番大事。AIが書いたテストをそのまま使うのではなく、テストの意図を理解して、不要なものを削り、足りないケースを追加する。

ステップ4: テストを実行して、失敗するケースを確認する。テストが全部通る場合は問題ない。失敗するケースがあれば、それがバグなのかテストの間違いなのかを判断する。

このサイクルを繰り返すうちに、テストを書く心理的ハードルがほぼゼロになった。

プロンプトの工夫で出力品質が変わる

AIにテストを書かせるとき、プロンプトの出し方で品質が大きく変わる。試行錯誤して見つけた、効果的なプロンプトのパターンをいくつか紹介する。

テスト観点を指定する。「正常系、異常系、境界値のテストを書いて」と明示するだけで、網羅性が格段に上がる。指定しないと正常系だけで終わることがある。

テスト名に意図を含めさせる。「テスト名は日本語で、何を検証しているか分かるようにして」と指定する。TestFunc_001 みたいな名前だと、後から見て何をテストしているのかわからない。Test_空文字を渡した場合にエラーを返す のように書かせると、テスト自体がドキュメントになる。

既存のテストファイルをコンテキストとして渡す。プロジェクト内の他のテストファイルを参考として渡すと、アサーションライブラリの使い方やヘルパー関数の活用が統一される。testifyを使っているプロジェクトなのに標準のtestingパッケージだけで書かれたりすることを防げる。

モックの方針を指示する。「外部API呼び出しのみモックして、内部のロジックは実際に実行させて」と明示する。これを言わないと、AIはとにかくモックで囲みたがる。

テストファーストに近い運用になった

AIでテスト生成が楽になった結果、実装前にテストを書く運用に変わった。

やり方はシンプル。まず関数のインターフェースだけ定義する。次にAIにテストを生成させる。最後にテストが通るように実装する。テスト駆動開発(TDD)に近い流れが、AIのおかげで自然にできるようになった。

テストが先にあると、実装のゴールが明確になる。「このテストが通れば完成」という基準があるのは精神的にも楽。しかも、仕様の曖昧な部分がテストを書く段階で浮き彫りになる。「この入力が来たとき、どうするのが正解なんだっけ?」と実装前に気づけるのは大きい。

自分は特にGoでこのフローを多用している。Goはインターフェースが明確に定義しやすいので、AIにコンテキストを渡しやすい。関数シグネチャとGoDocコメントだけ書いて渡すと、かなり精度の高いテストが返ってくる。

AIが書いたテストの注意点と品質管理

万能ではない。AIが書いたテストにも注意点がある。運用していく中で見つけた落とし穴をまとめておく。

テストの意図がわからない場合がある。AIは機械的にパターンを生成するから、「なぜこのケースをテストするのか」が不明確なことがある。テスト名やコメントで意図を明記させるのが大事。レビュー時に「このテストは何を守っているのか?」と自問して、答えられないテストは削除するか書き直す。

モックが過剰になりがち。AIは安全側に倒してモックを多用する。実際のDBやAPIを使った統合テストが必要な場面で、全部モックにされると本末転倒。モックだらけのテストは「テストは通るけど本番で動かない」という最悪のパターンを生む。

テストデータが非現実的なことがある。AIは "test""example" みたいなダミーデータを使いがち。実際のプロダクションデータに近い値を使わないと、文字コードの問題やデータ長の制限に引っかかるバグを見逃す。日本語の文字列、長大な入力、特殊文字を含むデータなど、現実的なテストデータに差し替えるようにしている。

既存テストとの重複。大きなコードベースだと、AIが既にカバーされている観点のテストを再生成してしまうことがある。生成前にカバレッジレポートを確認して、足りない部分を明示的に伝えると効率が良い。

テスト品質の変化

実際のカバレッジ改善事例

導入前はカバレッジ40%くらいだったのが、今は80%を超えている。コストはほぼゼロ。AIに生成させて、人間がレビューして、不要なものを削るだけ。

具体的な数値の変化を残しておく。自分が管理しているGoのCLIツール(約3000行)での結果。

  • 導入前: テストファイル3個、カバレッジ38%。主要な関数にだけ申し訳程度のテストがあった
  • AI導入1週間後: テストファイル12個、カバレッジ72%。既存コードに対して一気にテストを追加
  • 1ヶ月後: テストファイル18個、カバレッジ85%。新規実装分はTDDスタイルで最初からテストあり

カバレッジ100%を目指す必要はないと思っている。残りの15%は、テストしにくい外部連携部分やmain関数周り。無理にカバレッジを上げるより、重要なビジネスロジックが確実にテストされていることの方が大事。

テストがあると安心してリファクタリングできる。「この変更でテストが壊れたら問題がある」という早期発見の仕組みが、開発速度を上げている。実際にリファクタリング中にテストが3件失敗して、デグレを未然に防げた場面が何度かあった。

テストコードのAI生成を始めるならここから

テストをAIで自動生成する第一歩として、まずは既存のプロジェクトで一番テストがない部分を選んで、AIに投げてみるのがいい。完璧なテストを求めず、「ないよりマシ」のスタンスで始めると、心理的なハードルが低い。

テストを書かない理由はもうない。AIに任せればコストはほぼゼロだし、生成されたテストをレビューする過程で自分のコードの品質も見直せる。テストを書くのが嫌いな人ほど、AIに任せるべき。

関連記事

SharePost

他の記事