AIでテストカバレッジを23%から84%に引き上げた方法

1. はじめに

LITALICOナーシングネット開発チームの江口です。今回は、私が1年半かけて、試行錯誤しながら取り組んできたテストカバレッジ向上施策について紹介します。AI以前の取り組みから、AIにおけるプロンプトやスキル、うまくいかなかった点も含めて説明します。チームのテストを手厚くしたい、とお考えの方はぜひ参考にしてみてください。

2. AI以前: CIとテスト文化の土台づくり

2.1. 配属当初の課題

私がチームに配属されて、初めて環境構築を済ませた時のことを思い出します。単体テストを全件実行すると、いくつかのエラーが発生しました。プロダクトコードの変更に追従できず、メンテナンスされていないテストコードが残っていたのです。また、テストカバレッジを計測してみると、コード量25万行に対して全体のテストカバレッジは23%しかありませんでした。

品質上の不安を感じたのですが、一通り開発プロセスを体験してなるほどと思いました。

  • リリースサイクルを月次固定にすることで、テスト時間を確保する。
  • リリース前後で意図しない差分が発生していないか検証する「コンペアテスト」を行う。
  • 手作業テストは手厚く行う。

一連のプロセスは、早期に品質向上を図るため、結合テストを強化するという方針でした。しかし、結合テストで発見された不具合の修正は手戻りの負担が大きい傾向があります。より早い段階で不具合を発見するため、シフトレフトが必要だという課題はチーム内でも指摘されていました。

2.2. CIの導入

まず私はCIを導入しました。チームでは GitHub を使ってコード管理をしていたので、GitHub Actions を使って自動テストができるようにしました。CI設定については2025年のアドベントカレンダーに掲載しています。詳しくはこちらの記事をご覧ください。

https://zenn.dev/litalico/articles/20251221-github-actions-rspec

2.3. pending・flakyテストの整理

どうしても落ちてしまうテストは、テストに不備があるものとして pending のラベルを与え、実行しないようにしました。一定期間メンテナンスされなかった pending テストは、チームに確認をとった上で削除しました。

自動テストが動くようになった後も、パフォーマンスチューニングを行ったり、flaky test を修正したりしました。その過程で潜在的な不具合が見つかり、修正する場面もありました。flaky テストの修正技法については別の記事がありますので、関心がありましたらご覧ください。

https://zenn.dev/eggc/articles/20250210-fix-flaky-test

2.4. テスト文化の呼びかけと手書きの限界

CI導入にあたっては説明会を開き、重要性を説明しました。QAチームの結合テストでバグを発見した場合と比較すると、はるかに効率的です。開発だけで作業を完結でき、不具合解消のチケット管理も不要になります。忘れた頃に不具合が上がってくるのではなく、コードを書き換えたらすぐ不具合が見つけられるので、コンテキストスイッチも起こりにくくなります。開発チームにもQAチームにも良い効果が期待できます。最後に、とにかくテストコードを追加しましょうと呼びかけました。もちろん、自分が率先してテストコードを書くようにしました。

当時、プライベートの開発では生成AIを利用していましたが、業務においては会社のガイドラインに従い、手書きでの実装が主体でした。共通ヘルパーやテンプレートを用意し、「こういう型にはめれば簡単にテストが書ける」という土台を整えることに心を砕きました。協力してくれるメンバーもいましたが、広がりは限定的でした。それでも、手書きのテストを積み重ねて、全体のカバレッジは23%から42%まで向上しました。ただ、ここから先を人手だけで伸ばすのは遠い道のりだと感じていました。

3. AIでテストを書く

3.1. エージェントモードの登場

転機となったのは、全社的な GitHub Copilot の導入とエージェントモードの発表です。それまでAIといえばコード補完が主体で、それが限界でした。しかしエージェントモードでは、AI用のチャットウィンドウを開いて依頼するだけで、テストコードが生成できるようになりました。

私は、自分がテストコードを書く時にやっている事前調査やテンプレートを整理して、プロンプトとして書き上げました。試作したプロンプトは、AIエージェントにテストを書かせ、テストの実行まで動かすことができました。しかし、AIが以下のような不具合のループに陥り、先へ進めなくなる場面もありました。

  • 権限情報の一部がメタプログラミングで生成されていて、検索しにくく、AIがコードを追跡できない
  • 権限情報が正しく設定されていないと全てのリクエストが失敗するため、テストを成功させられない

どちらも人間がコードを読めば事情はわかりますが、AIには手がかりがありません。そこで、こうしたプロダクト特有の事情と対処法をプロンプトに書き込んでおくことで、同じ場所でのつまずきを防げるようになりました。

初期のプロンプトを受けたAIは、テストの成功を優先するあまり、モックやスタブを多用する傾向がありました。モックを多用すると、テストを通すことが簡単になる一方で、実際の挙動を確かめられなくなります。このプロダクトでは SQL や DB の挙動も含めてテストしたかったため、モックやスタブの利用をかなり制限するようにプロンプトを改善しました。あわせて、private メソッドは直接テストしないという方針も指示しています。これらの方針の背景は、6.1 で述べます。

3.2. Request Specを生成するプロンプト

具体的な例として、Rails の Request Spec を生成するプロンプトを紹介します。なお、テンプレート内の have_http_status(:ok) は最小限の例で、実際には異常系やレスポンス内容の検証も必要になります。プロダクトコードだけでなく、コマンドを使ってルーティングを調査することや、開発時のログから情報を集めることでより現実に近いテストパラメータを作れるようにしている点に注目してみてください。

## 対象

`spec/requests/**/*.rb` 配下のテストコードを作成・修正し、テストを成功させる場合に使用する。

## 目的

対象のコントローラに対する request spec を作成し、テストが成功することを確認する。

## 手順

1. 対象コントローラの routes を確認し、パスとアクションを特定する
2. 対象コントローラのコードまたは提供されたログから必要な params を特定する
3. 基本テンプレートに従ってテストコードを作成する
4. `docker compose exec web bundle exec rspec <TEST_FILE> --fail-fast` を実行する
   - 失敗した場合はトラブルシューティングに従って修正する
   - 再実行しても失敗する場合は、エラーログと解析結果を出力して作業を中断する
5. 成功後は `docker compose exec web bundle exec rubocop -A <TEST_FILE>` でコードを整形する

## 基本テンプレート

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Customer::SomeController, type: :request do
  include_context 'define session'

  let(:params) { { pref_id: pref_id, pref_code: pref_code } }

  describe 'index' do
    subject do
      get('/customer/some_path', params: params)
      response
    end

    it { is_expected.to have_http_status(:ok) }
  end
end

## テスト対象メソッド

- テスト対象はコントローラのアクションのみとする
- private メソッドは直接テストせず、アクションの振る舞いを通して確認する
- `docker compose exec web bin/rails routes -c Customer::SomeController` でアクションとパスを確認する
- アクションがない場合は直ちに作業を終了する

## リクエストパラメータ

- ログがある場合: `grep Customer::SomeController log/development.log | tail -n 20` でログを探索する。ログに記録されたパラメータ構成を再利用してテスト用パラメータを組み立てる。
- ログがない場合: コントローラの `params` の利用状況を調べてパラメータを組み立てる

## トラブルシューティング

(本セクションについてはプロダクトの固有情報が含まれるため今回の記事では非公開とさせていただきます)

3.3. コミットまで任せるスキル

私は、AIにテストコードを書かせた後、目視でレビューし、満足できたらコミットするという作業を繰り返していました。コミットメッセージにはチケット番号を記載するチームルールがあるため、単純なメッセージの自動生成には任せられません。しかし、ブランチ名からチケット番号を取り出すなど、コマンドを組み合わせれば自動化できることに気づきました。

そこで作成したのが以下のスキルです。git の操作には履歴を破壊してしまうリスクがあります。安全のため、スキルは「ユーザーから明確にコミット指示があった場合のみ」使う設計にし、git push や履歴の書き換え、変更の破棄は、明確な追加指示がない限り禁止しています。

---
name: git-commit
description: ユーザーから明示的にコミット指示があった場合のみ使用する
---

## 使用条件(必須)

- ユーザーから**明確にコミットの指示があった場合のみ**使用する
  - 例: 「コミットして」「ここまでをコミット」「コミットまでお願いします」
- 実装・修正・テスト追加が完了しただけでは**コミットしない**
- 判断が曖昧な場合はコミットせず、変更内容を報告するにとどめる
- 以下は**明確な追加指示がない限り禁止**
  - `git push`
  - `git commit --amend`、`git rebase`、`git reset --hard` などの履歴の書き換え
  - `git checkout`、`git restore` によるユーザーの変更の破棄

## 実行ステップ

1. 変更内容の確認: `git status --short` と `git --no-pager diff` で変更を把握する
2. チケット番号の取得: `git branch --show-current` からチケット番号(例: POS-1234)を抽出する
3. コミット対象の選択: 今回の作業に関係するファイルのみを `git add` する
4. コミットメッセージの作成: 後述の規約に従って組み立てる
5. コミットの実行と確認: コミット後に `git --no-pager log -1 --stat` で結果を確認し、ユーザーへ報告する

## コミットメッセージ規約

形式は `<チケット番号> <type>: <日本語の変更内容>` とする。

POS-1234 fix: 期間一括更新で開始日が未入力のときにエラーとなる不具合を修正

### type

以下のいずれかを用いる

- `feat`: 機能追加
- `fix`: 不具合修正
- `test`: テストコードの追加・修正
- `refactor`: 振る舞いを変えない内部改善
- `docs`: ドキュメント、プロンプト、skill の変更
- `ci`: CI 設定の変更
- `config`: 設定ファイルの変更
- `lint`: 静的解析の指摘対応

3.4. カバレッジを基準に反復させる

ある程度テストが充実してくると、正常系・異常系その他のコーナーケースも追加したいという欲求が高まってきました。そこで今度は、私がカバレッジを引き上げる時にやっていることを手順化してプロンプトに追加しました。

以下は simplecov により生成したカバレッジファイルから内容を読み取り、対象ファイルごとの行カバレッジが85%以上になるまでテストを繰り返すというプロンプトです。なお、カバレッジの値は coverage/index.html から取得しています。今回は人が見た時に視覚的にわかりやすく、対応が取りやすい index.html を使いましたが、simplecov のオプションを使えば機械的に読み取りやすい出力方法もあるかもしれません。

今回、85%という目標値は記事 テストカバレッジ100%を追求しても品質は高くならない理由と推奨されるカバレッジの目標値について | Qiita を参考に決めましたが、目的や背景によって変更しても良いと思います。

## カバレッジ向上のためのイテレーション

- テストコード実施のコマンドは以下のコマンドに変更する
  docker compose exec -e CI=1 web bundle exec rspec <TEST_FILE> --fail-fast
- 以下のコマンドによりカバレッジを取得する
  grep -A 1 <TEST_TARGET_FILE>.rb coverage/index.html | grep -oE '[.0-9 ]*%'
- カバレッジが 85% 未満の場合はテストケースを追加し、テスト実行から繰り返す

4. AIで量産し、仕組みにする

4.1. テストコード生成のバッチ化

AI生成の試行錯誤がひと段落し、一定の品質のテストコードが書けるようになってからは、プロンプトを繰り返し使いながら整備を進め、スキルに移植しました。今ではチャットウィンドウを開いて「ファイル○○のテストコードを書いて、完成したらコミットして」と依頼するだけで適切なスキルが自動的に読み込まれ、一定のテストカバレッジを保ちつつコミットまで自動化することが可能になっています。

ここまで人の手が離れてくると、機械的にテストコードを大量生産することが可能になりました。この段階では1回のプルリクエストで50個〜100個のテストコードを作成するようになっており、ファイル1つ1つを目視でレビューすることはほとんどなくなりました。

完璧なテストコードが生成されることは、最初から期待していません。品質は、自動テストだけで保証する必要はなく、手動テストなどの別の手法を含めて、設計できるからです。私たちのチームには、もともと手厚い手動テストがあります。その後ろ盾があったからこそ、まずはカバレッジを上げるという戦略をとることができました。

実際に私が使っているプロンプトは以下のように単純なものです。GitHub Copilot にこのプロンプトを渡すと、50個のファイルに対してテストコードを生成できます。実例として、GPT-6.1 Sol では50件のテスト生成に60分ほどかかりましたが、その間は別の作業をしていられるため、作業が終わる頃にはテストコードが完成しています。

以下に低カバレッジファイルのリストを渡します。まずはこれを記憶してください。

file/to/low_coverage_file01
file/to/low_coverage_file02
file/to/low_coverage_file03
file/to/low_coverage_file04
file/to/low_coverage_file05
...
file/to/low_coverage_file50

そして、上から1つのファイルに関してテストコードを生成してください。
作業が終わったらコミットをお願いします。作業が終わったファイルはリストから削除してください。
全てのリストが消化されるまでこの操作を繰り返してください。

4.2. バッチ化で得られた知見

バッチ化を試す中で、次のような知見が得られました。

1件ずつ逐次処理する

当初、カバレッジが低い10ファイルを入力し「まとめてテストを書いてください」という頼み方を試しました。すると、明らかに品質が劣化しました。テストの情報収集や設計の段階で扱う情報が多すぎて、コンテキストが錯綜しているように見えました。関連性のないファイルを並べるだけの入力は、難易度が高いようです。

そこで、プロンプトを1件ずつ処理する形に改め、それを繰り返させました。より高度なモデルを使ったり、関連性のあるファイル10個に固めたりすれば、コマンドの実行回数を省略できて、逆に効率的に動く可能性もあります。複数のモデルを自動で使い分ける GitHub Copilot Fusion や、サブエージェントでコンテキストを分離できる仕組みを使えば、この問題を減らせるかもしれません。

コンテキスト溢れに注意する

バッチ処理ではセッションが長引きやすいです。その結果、コンテキストウィンドウに情報が溢れ、コンパクションが実行されます。コンパクションが動くと、テストの品質が下がったり、依頼した手順を守らなくなったりします。これを避けるには、いくつかテストコードを書いたら別のセッションを開くのが無難です。

しかし、プロンプトを直接渡す方式からスキルに移行してからは、コンパクションが起きても指示が守られ、テスト品質が下がりにくい傾向があるように感じています。前述したバッチ処理でも、コンパクション後に大きな問題は起こりませんでした。ただ、これが仕様によるものかどうかは確認できていません。この辺りの挙動については今後も注目していきたいと思います。

GitHub Copilot と Claude Code

私は普段は GitHub Copilot を使って実験していました。Claude Code の Sonnet 5.5 でも、50件ほどのリストを1件ずつ処理させました。所要時間と品質のどちらも、大きな差は感じませんでした。テストコード生成中にプロダクトコードの不備を発見したときも、GitHub Copilot と同様に、不備があることを報告してくれました。

どちらも問題なくテストコードを出力してくれたのですが、Claude Code は定期的な進捗確認に応答するのが面倒でした。4〜5分おきに作業を停止し「N件終わりました、続けますか?」という確認が繰り返されてしまいました。これを軽減する方法として、質問に一定時間答えないと自動的に続行する askUserQuestionTimeout という設定があるそうです。次の実験ではこの機能も試してみたいと思います。

GitHub Copilot も似たような問題はあります。1回の依頼で行えるリクエスト数の上限を超えると、待ち状態になります。また、時間的なタイムアウトもあるため、長時間に及ぶ処理でも止まることがあります。件数の多いリストを処理させる場合は、これらの上限を引き延ばす設定をしておくとノンストップで走らせ続けることができます。

なお、コストについては、組織で管理されているため、私からは消費量を把握できませんでしたが、公開されている標準料金では、2つのモデルの単価は同じ水準です。今回の結果では、どちらのモデルでも問題ないと感じました。当面は両方試してみるつもりです。

安価なモデルで試す

安価なモデルの GPT-6 Luna を使った実験もしました。Sol に比べて生成に倍以上の時間がかかり、品質も落ちます。ただ、お手本となるコードがあれば、妥協できる品質のテストコードが生成されると感じました。Sol よりもはるかに安いので、コスト面で制約がある場合は役に立ちそうです。

4.3. 最終的なスキルの構成

スキルは Copilot や Claude など任意のエージェントから読み込めるように、シンボリックリンクを使うことにしました。

  • .agents/skills
    • git-commit
    • rspec
      • references
        • request-spec.md
        • …
  • .github/skills -> ../.agents/skills
  • .claude/skills -> ../.agents/skills

5. 成果と課題

5.1. 成果

今回紹介した手法で、全体のテストカバレッジは23%から84%まで向上しました。うち42%までは手書きでしたが、AI導入後には84%まで大きく伸びています。惜しくも目標値の85%にはあと1%届きませんでしたが、近いうちに達成できる見通しです。

カバレッジの向上は、プロダクトの品質向上にも貢献しました。カバレッジ向上のためにテストを追記すると、どうやっても到達できないデッドコードが発見され、リファクタリングのきっかけとなりました。

プロダクトコードの不備も発見できました。テストを書こうとしても常に失敗してしまう箇所が、ヒントになりました。AIエージェントが発見して報告してくれたものもありますし、テストコードのレビューで人が発見したものもあります。

Rails や Ruby などのバージョンアップ時にかかるテスト工数を減らすことができました。CI実行中に警告やエラーを検知できるため、手作業でしらみつぶしにテストをする手間を省略できるようになりました。

今回作成したプロンプトやスキルは属人性がなく、誰でも再利用できる点も重要です。たとえ私がいなくなったとしても、エージェントは今まで通り、期待するテストコードを書き出してくれるはずです。

5.2. 今後の課題

5.2.1. テストの質をどう確認するか

AIによるテストコードの生産能力は非常に高く、目視でのレビューは困難です。しかしレビューを無しにすると品質の問題が起こります。今回生成したテストコードには、価値のないものが含まれていることもありました。具体的には以下のようなテストケースです。

  • 常に500エラーを返すバグがあるプロダクトコードに対して、500エラーを返すことを検証する
  • ユーザ操作では通過できないプロダクトコードに対して、呼び出しを行い検証する
  • 現実ではありえないテストデータを使用して検証し、成功したものとみなす

テストが通っていること、カバレッジが上がっていることは、内容が正しいことの保証にはなりません。これらの穴を埋めるためにプロンプトやスキルを書き換えてきましたが、今後もそれが出力されない保証はありません。

こうした経験から、500エラーを期待するテストや安易なモック・スタブが含まれていないかを、grep による検索で確かめるようにしました。まだ同様の潜在的な問題が隠れているかもしれないので、時々、いくつかのファイルを目視で確認しています。このほか、重要な機能については目視での手厚いレビューを行ったり、生成AIとレビューAIを分けて協調させたりといった、次の一手を模索していきたいと思います。

5.2.2. E2Eテストへの展開

Playwrightによるテストをチームで作成中です。メンバーがAI用のスキルを整備して準備を進めているので、ここでも今回のように、手順を言語化してスキルとして共有する流れが活かせるのではないかと期待しています。ただ、E2Eテストは単体テストと性質が異なるため、以下のような未解決の問題が多くあります。

  • CIに組み込めていない: 現状は手動でトリガーしています。
  • コストが高くなりやすい: テスト環境を構築し、仮想的なブラウザを動かす必要があるため、リソースを多く消費します。
  • 並列実行できない: テストが1つのアカウントを前提としているため、テスト同士に暗黙的な依存関係があります。シナリオごとに異なるセッションを用意する仕組みも、まだ準備できていません。

こうした事情から、E2Eテストでは、単体テストのように、量でカバレッジを稼ぐ戦略は取りにくいと考えています。重要なシナリオに的を絞ってテストを生成し、実行するテストケースも絞り込む必要がありそうです。

5.2.3. テスト文化をどう育てるか

AIのおかげでテストの量は一気に増えた一方で、それを理解し保守できる人の力はまだ追いついていません。今このままカバレッジを高水準で維持できたとしても、プロダクト全体の品質管理や設計では、さまざまな種類のトラブルが発生します。たとえば、次のようなものです。

  • テストケースが現実のユースケースに即しているのか?
  • テストコードの実行は非効率になっていないか?
  • テストコードのコピペは増えすぎていないか?
  • テストが失敗したら、プロダクトコードを直すのか、テストコードを直すのか、ペンディングするのか?
  • CIが壊れた時にどうするか?
  • CIのコストが高くなったらどうするのか?

こうしたさまざまな課題に向き合う視座を持ってテストを運用していくには、仕組みだけでなくチーム全体の文化づくりが欠かせません。たとえば、テストコードも保守すべき資産であるという価値観や、CIを維持するためにコストを払うべきだという価値観をチームで共有できるようになれば、さらに強いチームになれるのではないかと思っています。

6. 考察と感想

6.1. なぜうまくいったと考えるか

今回うまくいった理由を、私は次の3つだと考えています。中でも3つ目は、人の役割に関わるため、少し丁寧に書きます。

1つ目は、経験則や知識をかみくだいて言語化し、再現性の高いコマンドへ翻訳したことです。まず、事前調査、テンプレート、実行と整形、行き詰まったときの振る舞い、カバレッジの確認と反復といった、普段は無意識に行っている手順を言葉にしました。次に、それを具体的なコマンドに置き換え、AIが失敗するたびにプロダクト特有の事情を足していきました。現場で人がやっていることをAIに再現させる、という素朴なアプローチは、成果を出しやすいのではないかと思います。

2つ目は、テストコードという題材の性質です。テストコードは、ゼロから何かを創作するのではなく、プロダクトコードの振る舞いを検査するものです。成功と失敗が明確で、終了条件も明快なので、AIに向いた仕事です。プロダクトコードを変更してユーザーに直接影響を与えることもないため、大胆に進めやすい領域だと思います。

3つ目は、トレードオフの判断を人が担ったことです。AIに任せられる作業が増えても、どちらを選ぶかという判断は人が行う必要があります。今回で言えば「モックやスタブはなるべく使わない」「private メソッドは直接テストしない」という方針です。

この方針は、SQL や DB の挙動を確かめたいという理由に加えて、私の経験則に基づいています。どちらも、テストがプロダクトコードの内部の振る舞いを決めつけ、リファクタリングの妨げになりがちです。また、モックは仕様変更に追従できず、プロダクトコードの結合部分が壊れていることを検知できません。

もちろん、逆の立場にも利点があります。モックやスタブを使えば、データベースへの依存が切り離され、前提レコードの作成に注意を払う必要がなくなり、テストの実行も速くなります。private メソッドをテストすれば、小さなテストをたくさん書くことができ、全体をまとめてテストする複雑さから解放されます。

こうした議論はインターネット上で何度も語られていて、AIも学習していると思われます。しかし、そこにあるのはトレードオフであり、正解はありません。何を重視するかを判断し、決定することが必要で、それは人がやるべきことなのだと思います。

6.2. 感想

実のところ、カバレッジ向上は私の主務ではありません。ただただ必要な仕事の一つとして地道に続けてきました。淡々とした繰り返しが、突如、AIの力で大きく動き始めました。私の知識を吸収したAIが、次々とコードを生産していくようになったのです。私は AI のチャットウィンドウを眺めながら、何か肩の荷が下りたような気持ちになりました。

今や私が介入しているのは、「どのファイルのテストを書くのか」という最初の一歩だけで、それを決めるのも難しいことではありません。この仕事に関しては、私の役割は確実に薄れています。ただ、仕事がなくなることに、強い恐怖は感じていません。人は、いろいろな経験を重ね、いろいろなことをこなせる存在です。そのうちどれか一つくらいは、AIより得意なことが残るのではないかと思っています。以前の節で書いた、判断と決定はその一つかもしれませんし、フレーム問題のように、何が関係あるのかを自分で見極め、取り組む範囲を自分で変えていくような部分もそうかもしれません。

7. おわりに

AIのおかげで、テストを書くことのハードルは大きく下がりました。特に、カバレッジが低く、まずは量を増やしたいという段階では、AIによるテスト生成は大きな力になります。この記事が、同じようにテスト不足に悩むチームの参考になれば幸いです。

次の課題として、テストの内容は本当に良いのか、人はAIの速さについていけるのか、という悩みが見え始めています。こうした課題に一緒に取り組み、AIとともにテストや開発の仕組みを整えていく仲間を探しています。興味を持たれた方は、エンジニア求人一覧をご覧ください。

最後までお読みいただき、ありがとうございました。