【RSpec】360秒→14秒!AIに丸投げして生まれた「激重テスト」を人間が爆速化するまでの全記録【Rails】
| 項目 | バージョン/詳細 |
|---|---|
| OS | Windows11 |
| 環境構築 | Docker Desktop (Compose) |
| Ruby | 3.2.2 |
| Rails | 7.2.1 |
| Test Framework | RSpec 3.13 |
1. はじめに:RSpecテストが遅すぎてスマホを触っちゃう話
RUNTEQというオンラインスクールでRuby on Railsの勉強を始めてから3か月の初学者、ヤドゥンです 🙋♂️
正直に告白します。私の開発中のアプリのRSpecテストコードは、ほとんど全てAIエージェントに書いてもらっています。
初学者にとって時間は有限であり、テストコードを書くのはハードルが高く、技術習得の優先順位を後回しにして、動けばOKとばかりにAIに頼り切っていたのです。その結果、テストコードはAIが生成したブラックボックスと化し、その実行速度はヤドン級に鈍足になってしまいました。
私 『なんかよくわかんねぇけど、Rspecがクッソおそい…
』
CIや手元でRSpecを実行すると毎回約3分~6分以上の待ち時間が発生。
「テスト実行待ちの間にスマホを見る」→「Twitter(X)を開く」→「集中力が切れる」という、エンジニアとして最悪の悪循環に陥っていました 🙅♂️📱
RSpecテストが遅いと、バグの特定や勉強集中のリズムが崩壊してしまいます。
このままでは、AIにテストを丸投げした結果、Webエンジニアとして戦う以前に 「遅さ」 の底なし沼から抜け出せなくなってしまう!
そう危惧した私はAIに頼り切る危険性を痛感し、テストを全面的に見直して「高速な開発サイクル」を取り戻すことを決意しました。
結論から言うと、最長6分以上(360秒超)かかっていたテストは、わずか14秒に短縮(約96%の高速化)されました!🚀
※ログ出力時間での計測です
この記事では、AIエージェントに依存していた私がどうやってこの爆速環境を実現したのか、AI生成コードに潜んでいた「潜在的な罠」や「FactoryBotの作りすぎ」をどう解消したかを含め、具体的なノウハウをすべて共有します。

2. 現状分析とAIに頼り切る危険性
高速化に着手する前に、まずはAI頼みだったテスト環境がなぜそこまで遅くなったのか、そのボトルネックを真面目に分析しました 💪
2-1. 初期環境と計測結果
当時の構成は Capybara + Selenium WebDriver(Chrome)によるシステムテストが中心でした。
| 項目 | 詳細 |
|---|---|
| システムテスト構成 | Capybara + Selenium WebDriver(Chrome) |
| テストケース総数 | 約 150 例 |
| テスト時間 (運よく成功) | 実測195秒 |
| テスト時間 (失敗時の最長記録) | 約 6 分以上(360 秒超) |
🐣 そもそも「Capybara + Selenium」とは何者か?
同じ初学者の方のために、この「定番セット」が何をしているのかを簡単に解説しておきます。
一言で言えば、 「人間の代わりにブラウザを操作してくれる透明なロボット」 です🤖
Railsのテストには種類がありますが、この組み合わせは System Spec(システムスペック) と呼ばれるテストで使われます。
-
Capybara(カピバラ):
- 「指令役」です。Rubyのコード(
click_on '保存'など)を読み取り、「ボタンを押せ!」「入力をしろ!」と命令を出します。
- 「指令役」です。Rubyのコード(
-
Selenium WebDriver(セレビニウム):
- 「操縦士」です。Capybaraの命令を受け取り、実際にGoogle Chromeなどのブラウザを動かします。
-
Chrome(ブラウザ):
- 「実行場所」です。私たちが普段見ている画面そのものです。
この3人が連携することで、 「ブラウザを立ち上げ、URLを開き、フォームに入力し、ボタンを押して、画面が変わったことを確認する」 という一連のユーザー操作を、全自動で行ってくれるのです。
JavaScriptを使った動的な動き(モーダルが出たり、画面遷移せずに保存したり)を確認できるため、現代のWeb開発には欠かせない最強のチーム……のはずでした。

2-2. Seleniumが抱える「構造的な遅さ」
AIエージェントは「動く」テストコードは生成してくれましたが、知識不足の初学者の『よしなにやってくれ』というアバウトな指示では、パフォーマンスや安定性については一切考慮してくれませんでした。
🐢 なぜ「Selenium」だったのか?そしてなぜ遅いのか?
そもそも、なぜ今までSeleniumを使っていたのか?
理由は単純です。「RailsのテストといえばSelenium」が定番だからです📚
多くの技術書でも、System SpecにはSelenium(Chrome Driver)を使うのが一般的です。学習歴3ヶ月の私が「とりあえずこれを使えばいいんだな」と信じ込むのは当然の流れでした。
しかし、この「定番」には、大規模なテストにおいて致命的な弱点がありました。
🛑 定番の罠:「通訳」だらけの伝言ゲーム
Seleniumは、Rubyのコードが直接ブラウザを操作するわけではありません。「WebDriver」という中間のプログラムを経由し、HTTP通信を使ってブラウザに指令を出します📡
イメージとしては 「通訳を介した伝言ゲーム」 です。
ヤドゥン
「このボタンのテストお願い」
Ruby(通訳1)
「了解。おいWebDriver、ボタン押せってさ」
WebDriver(通訳2)
「Yes, Sir. ブラウザさん、ボタンを押してください」
Chrome
「押しました」
WebDriver(通訳2)
「Rubyさん、押したそうです」
Ruby(通訳1)
「ヤドゥンさん、押したみたいです」
ヤドゥン
「(遅いなぁ……)了解」
このやり取りが、クリックひとつ、入力ひとつ発生するたびに行われるため、どうしても通信のオーバーヘッド(待ち時間)が発生してしまいます🐢
💤 毎回「カビゴン」のような鈍重さ
さらに、テストのたびにブラウザとドライバのプロセスを立ち上げる必要があり、これがテスト一つ一つに時間がかかる 「カビゴン」 ような鈍重さの原因でした。
1プロセス起動に約2〜3秒かかると言われており、テストケースが増えれば増えるほど、この「起動時間」が積み重なって全体の時間を圧迫していたのです⏰

🤖 AIが生み出した「逆ピラミッド」の罠
もう一つの大きな原因は、AIエージェントへの過度な依存です。
「ログイン機能のテスト書いて」とAIに頼むと、AIは喜んで以下のようなコードを書きます。
# AIが書きがちなコード(System Spec)
# 人間が読むには分かりやすいが、実行コストは高い
it "ユーザーがログインできること" do
visit login_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_button "Login"
expect(page).to have_content "ログインしました"
end
これは「System Spec」と呼ばれる、実際のユーザー操作をシミュレートするテストです。
本来、テスト戦略のセオリーでは、動作が高速な「Model Spec(単体テスト)」を多くし、重たいSystem Specは最小限にする 「テスティングピラミッド」 が理想とされています△
しかし、AIは 「動くけど重たいSystem Spec」を量産する傾向 がありました。
これはAIにとって、内部ロジックを検証するよりも、画面操作を記述する方が文脈を理解しやすいからかもしれません。
結果として、私のアプリはピラミッドが逆転し、最も重たいテストが最も多いという、 「超頭でっかちな構成(逆ピラミッド)」 になっていたのです
この「重すぎるSelenium」と「AI生成コードの最適化不足」を解消するため、根本的な環境変更とコードの見直しを決意しました。

3. 第1形態:Cupriteへの進化(カビゴン→サンダース)
「Seleniumが遅いなら、ツールを変えればいいじゃない」💡
AIに「速くて安定したドライバを教えて!」と質問した結果、提示された答え、それが Cuprite(キュープライト)でした。
起動に手間取る重量級の「カビゴン」(Selenium)をボールに戻し、素早さ雷速の「サンダース」(Cuprite)へのメンバー交代⚡️
このドライバ変更だけで、単一プロセスでの実行時間は、初期の不安定で失敗を乱発した360秒から、劇的に失敗が減少・安定化し、約120〜180秒まで一気に時短されました🚀
3-1. 技術解説:余計な手間を排除する CDP とは?
なぜCuprite(サンダース)はこれほど速いのでしょうか? その鍵は CDP(Chrome DevTools Protocol) にあります。
以前のSeleniumは、Rubyコードがブラウザに指令を出す際、WebDriverという「通訳」を経由してHTTP通信を使っていたため、クリックや入力のたびに余計な仲介役や手続き(待ち時間)が発生していました🐢
Cupriteは、このWebDriverという「通訳」を解雇し、CDPというChromeに実装されたAPIを通じて、Chromeの「脳みそ」(DevTools)に直接話しかけるようにします🗣️
途中に抽象レイヤーやツールを挟む必要がないため、処理が極限まで簡素化され、個別のシステムテストの実行速度が約30〜50%高速化されたという報告もあります。
これが今回の私のテスト環境にとって、まさに
きゅうしょにあたった! こうかは ばつぐんだ!▼
となったわけですね🎯
※Cuprite自体は、Ruby向けのCDPライブラリであるFerrumをCapybaraで使うためのGemです💎
3-2. 実装:トレーナー(Capybara)の相棒を入れ替える
Railsのテストにおいて、Capybaraは「ブラウザに命令を出すトレーナー」の役割を果たします🧢
今までトレーナーは「Selenium(カビゴン)」に命令していましたが、設定を変更し、使用するドライバ(相棒)を「Selenium(カビゴン」から「Cuprite(サンダース)」に切り替えます。
CIやDocker環境で安定させるためには、headless モードと、各種タイムアウトの適切な設定が重要です🛠️
# spec/support/cuprite_setup.rb (抜粋)
require 'capybara/cuprite'
# Capybaraに新しい相棒「Cuprite」を登録(Register)します
Capybara.register_driver(:cuprite) do |app|
Capybara::Cuprite::Driver.new(
app,
**{
# 1. 画面サイズの設定
# CI環境などでのレスポンシブ崩れによる操作ミスを防ぐため、明示的に指定します
window_size: [1200, 800],
# 2. ブラウザオプション
# ヘッドレスモード(GUIなし)で高速化
headless: true,
# Docker環境では 'no-sandbox' が必須。CIでのクラッシュを回避します
browser_options: ENV["DOCKER"] ? { "no-sandbox" => nil } : {},
# 3. タイムアウト設定
# Chromeの起動自体にかかる待機時間(CIなど遅い環境で重要)
process_timeout: 60,
# CDP通信(Ruby⇔Chrome間の命令)の待機時間
timeout: 30,
# 4. デバッグ用設定
# trueにすると、save_and_open_page でスナップショットを確認できます
inspector: true,
}
)
end
# Capybaraがメインで使用する相棒を「Cuprite」に指名します
Capybara.default_driver = Capybara.javascript_driver = :cuprite
# Capybara自体の待機設定
# 要素が見つからない場合、ここで指定した秒数だけリトライし続けます(デフォルト2秒→10秒へ)
Capybara.default_max_wait_time = 10
4. 第2形態:parallel_tests(かげぶんしん)
Cupriteへの移行で実行時間は大幅に短縮されましたが、人間の欲望は尽きません。
「テストをもっと速くしたい🤤」
そこで導入したのが parallel_tests というGemです。これはCPUのコア数に応じてテストを複数プロセスで並列実行する仕組みです。
例えるなら、 「かげぶんしん」 の術です🥷
1プロセス(1匹)で戦うのではなく、CPUのコア数に合わせて4プロセスに分身し、テストを分担して同時に実行する作戦です。
4-1. 並列実行の仕組みとデータベースの分離
parallel_testsは、RSpecファイルをバランスよくグループ分けし、プロセスごとに独立したデータベースを用意して同時に実行します。このデータベースの分離が、テスト間のデータの競合を防ぎ、不安定なテスト(フレーキーテスト)の温床を回避するために必須です🛡️
parallel_testsは、各テストプロセスに ENV['TEST_ENV_NUMBER'] という環境変数(空、'2', '3', '4'...)を自動で振ってくれます。この環境変数を利用して、config/database.ymlで接続先のデータベース名をプロセスごとに分けます。
# config/database.yml (抜粋)
test:
adapter: postgresql
encoding: unicode
# TEST_ENV_NUMBERを使って、プロセスごとにDB名を分ける!
database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>
pool: 5
これにより、rake parallel:setupコマンドなどで、テスト実行に必要な複数のDBが自動的に作成されます。
4-2. ツール導入直後に陥った「こんらん💫」状態
結論から先に言ってしまうと並列実行を導入した結果、最終的な実行時間は約14秒まで短縮されました⏱️
しかし、その14秒に至るまでの道は決して平坦ではありませんでした。
当然ながら、AIエージェントに丸投げして書かせたテストコードは、並列環境での動作など1ミリも考慮されていなかったのです。
準備不足のまま強引に「かげぶんしん(並列化)」した結果テスト環境はカオスと化し、並列化した途端、実行するたびにテストが落ちたり受かったりする「フレーキー(不安定)なテスト」が多発し始めたのです。
この時、私の脳内で再生されたのは、まさにあの絶望的なバトルログでした。
ヤドゥンは こんらんしている!▼
わけも わからず じぶんを こうげきした!▼
-
Ferrum::TimeoutError: Cupriteが原因不明でタイムアウトする問題が頻発⏳ -
PG::ConnectionBad: データベース接続エラーや競合 - アセット権限エラー: ファイルシステムへの書き込み競合によるエラー📂
さらなる高速化をめざしたつもりが、失敗リトライやタイムアウトの増加で「遅く、信用できない最悪のテスト」が爆誕してしまったのです。
私はAIエージェントの力に頼り切っていたことの危険性を痛感しつつ、 「安定化」 という次の敵に泥臭く立ち向かうことになりました![]()

5. 最終形態:AIコードの尻拭い(14秒の世界へ)
並列化(parallel_tests)を導入したものの、不安定さ(フレーキーテスト)という新たな壁にぶつかりました。この不安定さの真犯人は、外部ツールではなく、私自身がAIエージェントに頼り切った結果生まれたテストコードの中に潜んでいたのです🚨
それは「FactoryBotの乱用(作りすぎ)」です。
AIエージェントは、データの整合性や速度を考慮せず、ほぼ全てのテストケースで、DBへの永続化を伴う FactoryBot.create を行なっていました。これが並列実行時にDBのトランザクション競合や、システムテストにおけるタイムアウト(Ferrum::TimeoutError)の主犯でした。
5-1. 技術解説:FactoryBotの最適化戦略 ⚔️
テストの実行時間の大半は、無駄なDBへの永続化(書き込み)やデータ生成に費やされます。TestProfのツールで計測すると、テスト時間の90%以上がデータ生成にかかっているケースもありました。このボトルネックを解消するため、以下の3つの戦略を実行しました。
1. create から build_stubbed へ(DB書き込みの撲滅)
DBへのアクセスを避けることが、高速化の最も効果的な手段です。
-
モデルスペックでの活用: モデルのロジック検証など、データベースにデータを保存する必要がないテストでは、
createの代わりにbuild_stubbedを徹底的に優先しました。build_stubbedはメモリ上でのインスタンス生成にとどまるため、DBアクセスが発生するcreateに比べて10倍以上高速です。 -
戦略的な使い分け: DB保存が必須なシステムテストやリクエストスペックでは
createを使いますが、モデルの単体テストや関連付けのみ必要な場合はbuildやbuild_stubbedを使い分けます。
| メソッド | 処理内容 | DBアクセス | 速度(イメージ) | 推奨テストタイプ |
|---|---|---|---|---|
| create | DBに永続化(保存) | 必要 | 🐢(遅い) | システム/リクエストスペック |
| build | メモリ上の生成 | 一部あり | 🐰(中間) | モデルスペック(関連付け必要) |
| build_stubbed | メモリ上のダミー | 不要 | 🚀(高速) | モデルスペック(単体) |
2. 不要なアセット読み込みのブロック 🌐
システムテストの実行時、Webページが読み込む外部リソース(Google Fonts、画像など)が通信時間をわずかに遅延させていました。
- 解決策: Cupriteの設定を活用し、不要な外部リソースの読み込みをブロックするように変更しました。
- 効果: 1テストあたり約0.5〜1秒の短縮効果があり、並列実行環境では累積的に大きな効果を生みました。外部リソースへの依存を排除することで、テストの安定性も向上しました。
3. スリープ(sleep)の撲滅 ⏰
AIが生成したコードには、非同期処理の完了を「なんとなく」待つために、sleep 1やsleep 5といった命令が残っていることがありました。これは無駄な待ち時間を強制的に発生させ、テスト全体の実行時間を確実に延ばしてしまいます。
-
撲滅: 全ての
sleepを削除しました。 -
代替策: Capybaraには要素が表示されるまで待機する機能があります。
expect(page).to have_contentやhave_selectorといったhave_で始まるマッチャ、またはclick_onのようなメソッドは、要素が出現するまで必要最小限の時間だけ待機してくれます。これにより、無駄なく不安定さのない待機処理を実現しました。
# ❌ NG (AIが書きがちな無駄な待ち時間)
click_on "保存"
sleep 1
expect(page).to have_content("完了メッセージ")
# ✅ OK (Capybaraが要素が表示されるまで賢く待機する)
click_on "保存"
expect(page).to have_content("完了メッセージ")
5-2. 14秒の衝撃 ⚡
これらの地道で泥臭い修正(DB接続の安定化、キャッシュ設定の変更、そしてコードレベルの最適化)をすべて行った結果、ついにテストスイートの実行時間は、初期の360秒から安定して14秒を叩き出すことに成功しました🚀
これは初期状態から約96%の短縮です。フレーキーテストもほぼゼロになり、信頼性と速度を両立した、カップラーメンすら作れない爆速な開発環境が完成しました🍜🚫
| 段階 | 施策 | 実行時間 | 累積短縮率 | 安定性 |
|---|---|---|---|---|
| 初期状態 | Selenium(単一) | 360秒以上 | 0% | ⭐(超不安定) |
| Cuprite | ツール導入直後 | 180秒前後 | 50% | ⭐⭐⭐(安定小) |
| Cuprite + Parallel | ツール導入直後 | 210秒前後 | 42%(悪化) | ⭐⭐(不安定化) |
| フェーズ5完了 | 全ての最適化適用 | 14秒 | 96% | ⭐⭐⭐⭐⭐ |
6. まとめ:鈍足ヤドンからの卒業
6-1. AI依存と真の高速化の教訓
今回のテスト爆速化の旅を通じて得られた最も大きな教訓は、「AIの力は強力だが、それは魔法の杖ではない」 ということです。
Cuprite(軽量化)とparallel_tests(並列化)は実行時間を劇的に短縮してくれました。しかし、それらが提供するのはあくまで「高速化の土台」です。その土台の上で、AIが生成した「遅い、雑なコード」を、人間(ヤドゥン)が理解し、計測し、修正する力がなければ、テストは速く、そして安定したものにはなりませんでした。AIに頼り切る危険性を身をもって体験したのです。
6-2. AIエージェントとの新しい関係
AIは「動くコード」や「定型的なコード」を書くことは得意です。しかし、パフォーマンス最適化、DBアクセス戦略、並列環境での安定性など、文脈的な専門知識や品質要求が絡む「速くて安定したコード」を書くのは、まだ苦手です。
これからは、AIエージェントにテストコードを丸投げするのではなく、 「AIが出したコードを人間がレビューし、改善し、育てる」 という関係を築いていきたいです。AIはあくまで強力なアシスタントであり、最終的な品質と責任は私にあることを強く意識していく必要があります🔥
6-3. 結び:爆速環境で、学習と成長を加速させる
テスト実行待ちの時間は360秒から14秒へと劇的に短縮されました。もう、テスト実行中にスマホを見る暇はありません📱🚫
高速な開発サイクルは、バグの早期発見・修正を可能にし、開発体験そのものを向上させてくれました。
WEBエンジニアを目指す身として「高速な開発スキル」を武器に、これからもオンラインスクールでの学習を加速させていきます!


