AIコード生成に「秩序」を。900ファイルのテストを2ヶ月でRSpecへ完全移行した軌跡
1. はじめに
はじめまして!GA technologiesでソフトウェアエンジニアをしている柄本(えのもと)です。今年の4月で晴れて新卒2年目に突入しました!
今回は約900ファイルのテストを、AIを活用して2ヶ月強で完全移行したお話をさせていただこうと思います。
2. テストのあるべき姿
そもそも、エンジニアはなぜテストにこだわらなければならないのでしょうか。
エンジニアになりたての頃の私は、「本番コードの品質にこだわるのは当然だが、テストコードの品質にまでこだわるのは時間の無駄ではないか」と考えていました。しかし、実務経験を積む中で、その考えがいかに浅はかであったかを痛感することになります 😣
2-1. エンジニアが果たすべき責任
世の中には無数のプロダクトがありますが、いずれも、中長期的にわたって安全に動作し続ける保証など、どこにもありません。
さらに、エンジニア以外のステークホルダーにとって、プロダクトの「変更容易性」が維持されているか、あるいは組織の拡大に合わせて成長し続けられるかを判断するのは容易ではありません。
だからこそ、プロダクトが将来もバグを出さず、健やかに成長し続けられることを保証する責任は、私たちエンジニアが負うべきであり、それこそが、私たちがエンジニアとして存在する理由の一つだと考えます。
2-2. テストの品質が「競争力」を左右する
プロダクトの内側の問題は、外側から見ているだけでは決して分かりません。しかし、内部品質の欠如は中長期的に必ず顕在化し、最終的にはプロダクトの競争力そのものである「素早いデリバリー」をじわじわと蝕んでいきます。
つまり、ユーザーへの提供価値(外部品質)を高い状態で保ち続けるためには、土台となる内部品質を高めることが不可避なのです。そして、内部品質の状態を客観的に把握し、改善のための有益なフィードバックをくれる数少ないプロセスこそが「テスト」に他なりません。
したがって、開発者テストの本質的な目的とは、外部品質と内部品質の両面において、意思決定に必要な価値ある情報を提供することであると考えます。
3. なぜRSpecに移行したのか
3-1. 移行前のテスト品質の課題
秩序のないfixture
最大の問題はfixtureでした。
- 1つのfixtureファイルに数千行書かれている
- どのようなデータが用意されているのか把握しきれない
- テスト内で
delete_allしてデータを作り直しているテストもある - しかしfixtureを削除すると大量にテストが落ちる
fixtureが悪いというわけではなく、長い間fixtureを運用し続けて、手がつけられないくらいデータセットがぐちゃぐちゃになっていたことが問題でした。
minitest-railsというGemの問題
私たちが普段開発しているプロダクトではもともとminitest-railsというGemを使用していました。これは標準のMinitestをRSpecっぽく書けるGemですが、Rails 8.0へのアップデートの際に、minitest-railsの対応待ちで1〜2ヶ月のリリースブロックが発生しました。
テストスタイルの無秩序
テストのスタイルがバラバラでした。
- チーム内に「英語と日本語の間に半角スペースは設けない」というルールがあったが、テストによってはそのスタイルを守っていないケースがあった
- RuboCopがtest配下で無効化されていた(CIでlintが通らない状態だった)
3-2. テストの品質を向上させるために達成すべき4つの要件
テストの品質向上のために、以下の4つを達成すべき要件と定義しました。
| 要件 | 内容 |
|---|---|
| fixtureからFactoryBotへの置き換え | 複雑化しているデータセットを整理し、テストの見通しをよくする。 |
| GA technologies組織標準(RSpec)への準拠 | 組織のスタンダードであるRSpecへ統一し、技術的な意思決定やノウハウがスケールしやすい土壌を作る。 |
| minitest-railsからの脱却 | バージョンアップのブロックが起こりにくい、メンテナンス性の高いフレームワークへ移行する。 |
| スタイルガイドとガイドレールの構築 | CustomCopを作成し、AIによる生成も含め、常に一定の品質を保てる仕組みを導入する。 |
これらすべての要件を満たすためには、既存の仕組みを調整するレベルではなく、大規模な刷新が不可欠でした。そのため、組織としてのスタンダードであるRSpecへ完全に移行し切るという意思決定を下しました。
3-3. なぜ「今」RSpecに移行するのか
大規模な移行は多大な工数を伴いますが、なぜ「今」移行する決断をしたのでしょうか。その理由は、AIの台頭によってソフトウェア開発のコスト構造と前提が変わったことにあります。
AIによる「移行コスト」の劇的な低下
かつては苦行だった数千ファイルのコード変換も、現代のAIにとっては得意分野である「高度なパターン翻訳」に過ぎません。これまでは「工数が見合わない」と切り捨ててきた大規模リファクタリングが、AIをパートナーにすることで、かつてないほど低い投資コストで実行可能になりました。
AIの「黎明期」だからこそ、AIが真価を発揮するための土壌を整える
AIは魔法ではありません。高品質なアウトプットを引き出すには、明確な「型」と「規約」というガイドレールが不可欠です。カオス化した既存コードを学習・模倣させ続けても、負債が再生産されるだけです。
ここで重要なのは、これまでチーム内で「なんとなく」共有されていた、あるいは個人の感性に委ねられていたいい書き方(暗黙知)を徹底的に明文化し、AIが理解可能な規約(形式知)へと落とし込むことです。
AI開発フローへの移行期である今、この明文化こそがAIのポテンシャルを最大限に引き出すための必須条件となります。AIを中心とした開発体制になっても、品質がぶれない土壌を整えることがエンジニアにとっての急務であると考えました。
4. どうやって900ファイルを移行したか - AIを活用した移行戦略
900ファイルの移行で達成すべきことは、大きく2つあります。
- 偽陰性の排除 — 移行によってテストの検証精度が落ち、本来失敗すべきテストが通ってしまう事態を防ぐこと
- スタイルの統一 — プロダクト内で「良い書き方」に統一し、AIが書いても人間が書いても一定の品質が保たれる状態を作ること
この2つの目的を達成するために、以下の3つの仕組みを構築し、さらにそれらを人間のレビューを通じて継続的に改善するサイクルを回しました。
- Rulesが「AIにスタイルを教える」
- CustomCopが「ルール違反を機械的に検出する」
- Commandsが「作業手順を標準化する」
- 人間のレビューが「最後は責任を持ち、仕組みそのものを磨き上げる」
4-1. CustomCop - 品質を機械的に担保する
人間のレビューに頼るだけでは、900ファイルの移行で品質を保つことは困難です。CIで自動的にルール違反を検出するCustomCopを、前述の2つの目的に沿って整備しました。
目的1: 偽陰性の排除
大規模移行において警戒すべきリスクは偽陰性、つまり「本来失敗すべきテストが通ってしまう」状態です。フレームワークの変換過程で、テストの検証精度が意図せず低下し、バグを見逃すテストが量産されてしまっては移行の意味がありません。
このリスクに対して、社内の他チームのリポジトリを参考にしながら、期待値の曖昧さそのものを機械的に排除するCustomCopを導入しました。
RspecAvoidBeTruthy - be_truthy / be_falsy の禁止
# NG — be_truthyはnilとfalse以外のすべてを通す(0, "", [] も通る)
it { is_expected.to be_truthy }
# OK — 真偽値を厳密に検証
it { is_expected.to be true }
be_truthyはnilとfalse以外のすべての値を「成功」と見なします。つまり、メソッドがtrueを返すべきところで0や""を返していても、テストは通ってしまいます。be true / be falseを強制することで、期待する型と値の両方を厳密に検証します。
AvoidBeEmpty - be_empty の禁止
# NG — be_emptyは配列もハッシュも文字列も通す
it { is_expected.to be_empty }
# OK — 戻り値の型を含めて厳密に検証
it { is_expected.to eq([]) }
it { is_expected.to eq({}) }
be_emptyはempty?メソッドがtrueを返すあらゆるオブジェクトにマッチします。本来[](空配列)を期待しているテストが、実装のバグで""(空文字列)を返していても通ってしまいます。eq([])やeq({})を強制することで、戻り値の型まで含めた厳密な検証を実現しています。
目的2: スタイルの統一
書き方やスタイルに関しては人によって「正解」が異なる分野だと思います。これから紹介するいくつかのCustomCopも人によっては「こっちの方がいい」という意見もあるかと思います。しかし、大切なのはプロダクト内で書き方が統一されていることです。
RspecRequireSubject - subjectの明示でテストの「仕様書化」を強制
テスト対象のメソッド呼び出しをsubjectで明示的に宣言することを義務化するCopです。
# NG — 何をテストしているか一目でわかりにくい
describe '#full_name' do
it { expect(user.full_name).to eq '山田太郎' }
end
# OK — subjectで「これをテストしている」が一目でわかる
describe '#full_name' do
subject { user.full_name }
it { is_expected.to eq '山田太郎' }
end
テストを「読めばメソッドの仕様がわかる」状態にするための仕組みです。subjectを見れば何をテストしているかがわかり、itを見れば期待する振る舞いがわかる。この構造を機械的に強制しています。
RspecMustUseFactory - データセットの定義の際にFactoryBotの使用を強制する
テストファイル内でモデルクラスに対する直接的なデータ操作(Customer.create!等)を検知し、FactoryBotの短縮構文(create(:customer)等)の使用を促すCopです。
# NG — モデルクラスへの直接呼び出し
Customer.create!(name: '太郎', email: 'test@ga-tech.co.jp')
Customer.delete_all
# OK — FactoryBot短縮構文
create(:customer, name: '太郎', email: 'test@ga-tech.co.jp')
ただし、subjectブロック内での呼び出しは例外としています。これはテスト対象のメソッド呼び出しであり、データセットの投入ではないためです。
# OK — subject内はテスト対象のメソッド呼び出し
subject { customer.update(id, name: '次郎') }
UseApiMockModule - APIモックの散在を防ぐ
stub_requestの直接呼び出しを検知し、spec/api_mock/配下のApiMockモジュールの使用を促すCopです。
# NG — stub_requestが散在するとメンテナンスが難しくなる
stub_request(:get, 'https://api.example.com/users/1')
.to_return(status: 200, body: { name: '太郎' }.to_json)
# OK — ApiMockモジュールに集約
ApiMock::Account::StubGetUser.call(status: 200)
外部APIのモックが各テストファイルに散在すると、APIの仕様変更時に全ファイルを修正する必要が出てきます。1箇所に集約することでリファクタリング耐性が向上します。
そのほかにも私たちのチーム以外で開発されている他プロダクトを参考にしながら、CustomCopをいくつも追加しています。
4-2. Rules - AIが参照する「スタイルガイド」の整備
AIに高品質なテストを書かせるためには、場当たり的な指示ではなく、一貫した「指針」が必要です。そこで私たちは、Claude Codeが参照するRules(.claude/rules/)として、詳細なスタイルガイドを整備しました。
チームの「暗黙知」を言語化するプロセス
このガイドラインは、単にドキュメントを書き起こしただけではありません。マネージャーや先輩からの「AI時代だからこそ、土台となる規約の純度を高めるべき」という助言を受け、チーム内で徹底的なディスカッションの場を設けました。
- 読みやすいテストの条件とは何か
- リファクタリング耐性が高いテストとは何か
- 誰が書いてもテストの質が担保されるような仕組みはないか
こうした問いに対し、メンバー全員でミーティングを重ね、納得感のある「理想の書き方」を合意形成しました。このプロセスを経て言語化されたチームの結晶をRulesに落とし込むことで、AIは私たちの期待に沿ったコードを出力できるようになりました。
整備した4つの主要Rules
| ファイル | 役割 |
|---|---|
| rspec_common.md | 全テスト共通の基本原則。メールドメインの統一、半角スペースの徹底、期待値のベタ書き推奨など。 |
| model_spec.md | Model Specの構造定義。「1 it = 1 expect」の厳守や、is_expected と subject の使い分けなど、一貫した検証スタイルを規定。 |
| request_spec.md | Request Specの構造定義。 |
| factorybot.md | FactoryBotの運用ルール。複雑なtraitを避け、最低限の属性定義に留める。 |
4-3. Commands - 移行作業の自律化
Claude CodeではカスタムCommands(Skills)を作成して、AIの作業をフロー化できます。
/rspec_migration — 移行の「レシピ」をコード化
1ファイルの移行手順を4フェーズに定義したコマンドです。
フェーズ1: RSpec変換
1. Rulesを読み込む
2. MinitestをRSpecスタイルに変換
3. 移行等価性チェック
4. テスト実行 → エラーがあればテストコードのみ修正 → 再実行
※ 本番コード(app/配下)は絶対に変更しない
フェーズ2: RuboCop修正
5. RuboCop実行 → エラーがあれば修正 → 再実行
6. 合格するまで繰り返す
フェーズ3: 最終確認
7. テストとRuboCop両方が合格することを確認
8. 移行前後のカバレッジを計測・比較
フェーズ4: クリーンアップ
9. 元のMinitestファイルを削除
/operate_rspec_migration — issueからPR完成まで全自動
/rspec_migrationをさらに上位で包括するコマンドです。人間がGitHub issueを作ってこのコマンドを実行するだけで、PRが出来上がり、レビュー対応まで自律的に行います。
フェーズ1: 作業着手
- issueにコメント、Assignee設定
フェーズ2: 作業計画の策定
- 対象ファイル一覧、移行順序、PR方針、リスクを整理
フェーズ3: RSpec移行の実行
- /rspec_migration を実行
フェーズ4: PR作成
- developブランチに対してPRを作成
- 移行前後のカバレッジ比較テーブルをPR概要に記載
フェーズ5: レビュー・CI対応
- セルフレビュー実行
- Copilotレビュー待ち(15秒間隔でポーリング)
- レビューコメント対応(最大3ループ)
- CircleCI MCPでCI確認 → 失敗時はre-runまたはコード修正
フェーズ6: 完了報告
- PRに対応完了コメントを投稿
このコマンドの特徴は、前述の2つの目的(偽陰性の排除とスタイルの統一)を達成するためのレビューループです。
CustomCopはCIで機械的にルール違反を検出しますが、それだけでは「移行前のテストが検証していた内容を、移行後も同等の精度で検証できているか」というテストの等価性までは保証できません。
さらに、移行元のテスト自体にすでに偽陰性が潜んでいた場合、単純に等価変換するだけではその問題ごと引き継いでしまいます。
そこで、フェーズ5ではまずClaude Code自身がセルフレビューでテストに問題がないかチェックし、その後GitHub CopilotやBugBotによるクロスレビューを挟みました。
4-4. 人間のレビューによる仕組みの継続的改善
3つの仕組み(Rules・CustomCop・Commands)は、最初から完成形だったわけではありません。人間がレビューで問題を発見し、その学びを仕組みにフィードバックする改善サイクルを回し続けることで、品質の底上げを実現しました。
レビュー → 改善 → テスト作成の反復ループ
このサイクルを繰り返すことで、移行が進むにつれてAIの出力品質は向上していきました。
いくら仕組みが解決するといっても、人間による最終レビューは大切です。テストの品質向上のために人間が最後は責任を持ってレビューしました。
5. まとめ
5-1 RSpec移行を完遂してみて
かつては人間の手だけでは相当の工数を要したであろう「テスティングフレームワークの完全移行」を、AIをフル活用することで2ヶ月強という短期間で成し遂げることができました。
AIによって移行コストの前提が劇的に変わったことは大きな転換点です。しかし、AIにただ丸投げするだけでは、真の意味で「価値のあるテスト」は生まれません。Rules(スタイルガイド)、CustomCop(機械的検査)、そしてCommands(手順の標準化)という3つのガイドレールと、人間によるレビューループが、品質を維持しながら爆速で移行を完遂できた真の鍵でした。
5-2 エンジニアの責任、AI時代の決意
「テストのあるべき姿」で述べた通り、プロダクトが将来にわたってバグを出さず、健やかに成長し続けられることを保証することは、私たちエンジニアが果たすべき不変の責任です。
AIを活用して開発生産性を向上させることは重要ですが、どれほどツールが進化しても、最終的な品質への責任を肩代わりしてくれるわけではありません。むしろ、AIがコードを生成する時代だからこそ、私たちはこの責任をこれまで以上に全身全霊で果たす必要があります 💪
今回の移行プロジェクトを通じて、私は確信したことが一つあります。それは、テストの品質を担保し向上させ続ける取り組みこそが、プロダクションコードの品質と不可分であり、プロダクトの成長を支える上で欠かすことのできない「絶対的なパーツ」であるということです。
そのため、今後の挑戦の一つとして、テストの品質そのものをAIやその他ツールを活用して可視化・定量化できないかを模索していきたいと考えています。カバレッジ率だけでは測れない「テストの検証精度」や「偽陰性のリスク」を数値として捉えられるようになれば、テストの品質、さらにプロダクトの品質を向上させることができるはずです。
謝辞
今回のRSpec移行は、多くのメンバーの支えなしには実現できませんでした。
移行作業を自動化するコマンド /operate_rspec_migration をはじめ、AI開発を支える数々の画期的なツールや仕組みを提案・実装してくださり、積極的にAI開発の土壌を作ってくださった先輩には感謝してもしきれません。実際にかなりのファイルをRSpecに移行してくださいました。
さらに、技術的な挑戦を「組織の未来への投資」として快くサポートしてくださった本部長とマネージャー。そして、新しいテストスタイルを共に磨き上げ、他の開発などでお忙しい中、粘り強く移行作業を協力してくれたチームメンバーのみなさん。心から感謝いたします!
また、本記事の作成にあたっては、社内勉強会で共有されていた内容をかなり参考にさせていただきました。ありがとうございます。
おわりに
GA technologiesでは、AIの利活用や、プロダクト・テストの両軸から品質向上を推進するソフトウェアエンジニアおよびエンジニアリングマネージャーを募集しています。
もし興味があればぜひ下記の採用ページからご連絡ください!
Discussion