
はじめに
こんにちは、グローバルシステム部フロントエンドブロックの林です。
私が所属するチームではZOZOMETRYというBtoBサービスを開発しています。スマートフォンで身体を計測し、計測結果を3Dモデルやデータとして可視化・Web上で管理できるサービスです。
私たちのチームではAIにユニットテストを書かせ、マージまでの過程を改善する施策を実施しました。結果としては、2か月でテスト数が57%増え、カバレッジは約2倍になりました。
この取り組みはテストを増やすという面ではうまくいきましたが、AIが書いたコードを人間がどうレビューするかという点で、いくつかの壁にぶつかりました。
この記事では、以下の点を紹介します。
- AIが書いたテストコードを素早くレビューするために、どのような仕組みを設計したのか
- 運用する中でどのような課題が見えてきて、どう対処したのか
- AIと協業する開発フローにおいて、人間が関与すべきポイントはどこだったのか
目次
背景と課題
私たちのチームでは、機能開発を優先するあまりテストが慢性的に不足しており、以下のような課題が続いていました。
- 品質管理はQAチームに大きく依存している状態
- テスト作成の品質や粒度にばらつきがある
- テストの目的や内容を理解するためのドキュメントが十分に整備されておらず、「このテストは何を守っているのか」を説明しにくい
施策の開始時点でのテスト数は324件、カバレッジは4.72%でした。
この状況を改善するにあたって、いくつかの選択肢がありました。人手でテストを書くのが最も確実ですが、機能開発と並行して進めるリソースがありませんでした。AIにテストを生成させれば速度は出ますが、品質の保証は未知数です。
結果として、AIにテストコードを生成させ、人間がレビューする体制を選びました。とはいえ、最初からAIに品質を丸投げできるとは考えていませんでした。この実験にはもう1つの目的がありました。AIと協業するうえで、人間が関与すべきポイントはどこなのか。それを見出すための取り組みでもあったのです。
テスト生成の仕組み
テスト生成の仕組みを以下の3点で構成しました。
- Claude Codeコマンドによるテスト生成の定型化
- 統一フォーマットによるテスト構造の標準化
- テストサマリの自動付与
Claude Codeコマンドの設計
Claude Codeのカスタムコマンド/create-unit-testを作成しました。このコマンドは対象ファイルのパスを受け取り、以下のワークフローを順に実行します。
- 対象ファイルの分析:ファイルタイプ(フック / ユーティリティ / ストア / コンポーネント)を特定し、エクスポートされる関数の一覧や依存関係を把握する
- テスト設計書の作成:
docs/test-design/にテスト設計書を生成し、テストケースを正常系・異常系・エッジケースに分類する - テストファイルの作成:設計書に基づいてテストコードを
test/unit/に配置する - テスト実行と検証:
pnpm testでテストを実行し、カバレッジを確認する - テストサマリの記録:
docs/test-summaries/test-summary.mdにテスト内容を追記する
# 実行例
/create-unit-test hooks/useClientData.ts
/create-unit-test utils/detectGender.ts
各ステップでユーザーの承認を挟む設計にしています。AIに一気に生成させるのではなく、分析→設計→実装→検証の各段階で人間が判断する余地を残しました。
コマンドの設計で重視したのは再現性です。誰が実行しても同じ粒度・同じ構造のテストが生成されることで、レビューする側の認知負荷を一定に保つことを狙いました。
統一フォーマット
生成されるテストの構造を揃えるために、以下のルールを定めました。
describeのネスト構造
テスト対象の関数ごとにdescribeをグループ化し、その中をSuccess case / Error case / Edge casesに分類します。
describe('useCreateClient', () => { describe('Success case', () => { ... }); describe('Error case: Argument problems', () => { ... }); describe('Error case: Response errors', () => { ... }); describe('Edge cases', () => { ... }); });
この構造が揃っていることで、レビュアは「このテストはどの分類のケースを見ているのか」をコードの構造から即座に判断できます。
テスト名と日本語コメント
テスト名はshould [期待される動作]の形式で統一しました。加えて、各describeやitの前に日本語コメントを付けることで、テストの意図をコードを読み込まずとも把握できるようにしています。
// 性別判定機能のテスト describe('detectGender', () => { // 男性の場合、正しいメッセージを返すことを確認 it('should return the correct message for MALE', () => { expect(detectGender('MALE')).toEqual('Male'); }); });
テスト対象ごとの実装パターン
対象のファイルタイプに応じて、テストの書き方を使い分けています。テストケースが少ないフックにはrenderHookを使い、セットアップを簡潔に保ちます。テストケースが多いフックには直接呼び出しとdescribeのネストを組み合わせ、テストケースごとの独立性を確保します。ユーティリティ関数は入力と出力の対応を直接検証し、Zustandストアはactで状態更新をラップすることでReactの非同期性に対応しています。
この使い分けもコマンド側で自動的に判断するため、生成されたテストのパターンがばらつくことを防いでいます。
テストサマリの付与
テスト実行後、docs/test-summaries/test-summary.mdにサマリを追記する仕組みを導入しました。サマリには以下の情報を含めています。
- テスト対象ファイルとタイプ
- テスト内容:関数シグネチャと、どの分類(正常系・異常系・エッジケース)をテストしたか
- テスト結果:成功数 / 全体数
以下は実際のサマリの例です。
## `utils/fileName.ts` - 2025-12-04 14:28:00 **タイプ**: ユーティリティ **テストファイル**: `test/unit/fileName.test.ts` ### テスト内容 - `getDisplayFileName(name, maxLength?, headLength?): string` - 正常系(短い/長いファイル名、デフォルトパラメータ、境界値)、エッジケース(空文字列、日本語) - `isValidFileName(name, maxLength?, includeExtension?): boolean` - 正常系(英数字・日本語・記号)、異常系(不正な拡張子、長さ超過)、エッジケース(複数ドット、最小長) **結果**: ✅ 全テスト成功 (32/32)
このサマリはPRのレビュー時にも参照します。レビュアはまずサマリを読んでテストの全体像を把握した後で、実際のコードに問題点がないかを確認するフローにしました。
成果
2か月の実施期間で、ユニットテスト数は324件から509件へ57%増加しました。カバレッジは4.72%から9.25%へ、約2倍に改善しています。
定量的な成果に加えて、以下の定性的な改善もありました。
- テスト設計書とサマリが蓄積されたことで、テストの目的やカバー範囲をチーム全体で把握できるようになった
- テストの構造が統一されたことで、レビュー時に「何を見ればいいか」が明確になった
- 既存テストの品質を見直すきっかけにもなった
運用で見えた課題
成果は出ましたが、運用する中でレビュー面の課題が顕著になりました。課題の本質は「AIの出力品質」ではなく、正しいと判断するための「検証コスト」にありました。
AIの生成速度と人間のレビュー速度のミスマッチ
AIによりPull Request(以下PR)の生成時間が大幅に短縮されたため、未レビューのPRが溜まるようになりました。PRを作った側にはレビュー依頼やリマインドへの心理的障壁が生まれました。レビューする側も次々と届くPRにプレッシャーを感じる状態でした。この状態でチームの生産性を最大化するのは難しいものでした。
「ノールックでマージするのは怖い」
AIが書いた、品質に直結する部分のコードをノールックでマージするのは怖いと感じました。チームで話し合った結果、人間が差分を目視で確認することにしました。
しかし目視確認にも課題が隠れていました。PRの粒度が大きくなりがちで、人間の認知負荷が増加したのです。
「インプットとアウトプットだけ見ればいい」仮説の崩壊
CI/CDで実行を管理しているので、変更されたコードを見なくてもインプット(プロンプト)とアウトプット(テスト実行結果)だけ確認すればいいのではないか。そういった仮説を立てました。
しかし現実には、インプットが本当に期待しているインプットなのかを判断するためのコンテキストが属人化していました。設計や詳細なコードを把握していないメンバーは自力で調査する時間が増え、かえって非効率になりました。この状態を改善しなければ、サービスの品質向上や本質的な改善は難しい状況でした。
課題への対策
これらの課題に対して、3つの施策で対処しました。
- サマリの自動生成
- AIにプランニングさせ粒度を制御する仕組み
- 人間が差分を目視で確認するプロセスを明示的に残す
サマリの自動生成でレビューの入口のハードルを下げる
テストされている箇所の設計や実装を把握していないメンバーでもレビューに入りやすくすることを目的としています。前述のサマリを活用したレビューフローを通じて、不慣れな領域でもテストの全体像をあらかじめ把握した状態でコードレビューへ臨めるようにしました。
これにより、不慣れな領域のレビューに対する心理的障壁を軽減し、迅速にレビューへ入れるようになりました。
粒度の制御でレビュー1回あたりの負荷を下げる
コマンド実行時、どの範囲のテストを作成するかAIへプランニングさせる仕組みにしました。PRサイズは100行程度を目安に設定しています。
テストカバレッジを一度に大きく上げたくなりますが、レビューする側の認知負荷を超えないことでレビューに臨むハードルを下げることができました。
目視確認のプロセス化
「ノールックでマージしない」というチームの方針に基づき、人間が差分を目視で確認するプロセスを明示的に残しました。AIの出力を無条件に信頼するのではなく、品質の最終判断は人間が担う体制です。
これらの改善施策により、レビューまでのリードタイムが減りメンバーの心理的な負担も少なくなりました。
振り返り:AI協業における人間の関与ポイント
この実験を通じて、AIと協業する開発フローにおけるいくつかの知見が得られました。
AI生成コードのレビューで人間が見るべき範囲
「インプットとアウトプットだけ見ればいい」という仮説は成立しませんでした。コンテキストの共有が前提条件として必要であり、それが属人化している状態では、コードの差分を目視で確認する以外に品質を担保する手段が見つかりませんでした。
チームが出した結論は「差分のコードを目視で確認するのは、やはり人間が担当すべき」というものです。レビューのコストが上がる課題は引き続き残りますが、品質の担保を優先しました。
生成速度とレビュー速度のバランス設計
AIの生成速度に人間が追いつけない構造的な問題に対しては、生成側で粒度を制御することが有効でした。レビュー側の運用を変えるのではなく、生成側の出力を調整するアプローチです。
導入コストを下げるアプローチ
完全に新しいプラクティスを一から導入するのはコストが高いため、現行の開発フローをコンポーネント化し、AIに任せられる部分だけを切り出すアプローチを取りました。大きく変えるのではなく、今あるものの一部を置き換えていく形です。
まとめ
AIにテストコードを生成させる施策を通じて、テスト数を57%増やし、カバレッジを約2倍に改善しました。一方で、運用面の課題も見えてきました。AIの生成速度に人間のレビューが追いつかないこと、コンテキストの属人化によりインプット/アウトプットだけでは品質を担保できないことです。
これらの課題に対しては、サマリの自動生成と粒度の制御という仕組み側の改善で対処しました。しかし「人間が差分を目視で確認する」という部分は残しています。ここを自動化できる条件は、まだ見出せていません。
AIと協業する開発フローにおいて、人間が関与すべきポイントはどこなのか。この問いに対する私たちの暫定的な答えは、「コードの差分を確認し、品質を判断すること」です。この判断を下せるのは、コードを書いてきた経験の上に成り立つ審美眼があるからだと考えています。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。