Railsのシステムテスト入門【RSpec × Capybara】|ブラウザ操作を自動テストして「壊れてないか」を担保する

rspec spec/system を実行し、ログイン・投稿作成・一覧表示の3つのsystem specがすべて緑(3 examples, 0 failures)になっているターミナル画面
  • このエントリーをはてなブックマークに追加

スポンサーリンク

「一覧画面やフォーム、リリースのたびに手で動かして確認するの、地味に面倒じゃありませんか?」——モデルの単体テストは書いているけど、「ユーザーが実際にブラウザで操作したときにちゃんと動くか」までは手作業、というチームは多いはずです。

そこで使うのが RSpec のシステムスペック(system spec)です。ログインして、フォームに入力して、ボタンを押して、画面に結果が出る——この一連の操作をそのまま自動テストにできます。この記事では、rspec-rails と Capybara のセットアップから、最初の1本を書いて緑にするまで、そして「なぜか落ちる」時の対処や CI での動かし方までを、実務でテストを書いてきた立場で整理します。読み終わる頃には、あなたのアプリに system spec を1本追加できるようになります。

まず結論:system spec で「手動確認」を自動化できる

細かい設定は後で説明します。先にゴールのイメージだけ渡しておきます。system spec を1本書くと、次のような「人がブラウザでやっていた確認」が rspec コマンド一発で緑になります。

rspec spec/system を実行し、ログイン・投稿作成・一覧表示の3つのsystem specがすべて緑(3 examples, 0 failures)になっているターミナル画面
system spec を書くと、手作業だった「ログイン→投稿→一覧確認」がコマンド一発で緑になる

手動確認との一番の違いは、失敗したときに自動でスクリーンショットが保存されること。Rails 5.1 以降の「システムテスト」がベースなので、テストがコケた瞬間の画面をあとから見返せます。「CI では落ちるけど手元だと再現しない」の調査がぐっと楽になります。

こんなときsystem spec が効く
ログイン→投稿→一覧表示、のような画面をまたぐ操作を担保したい◎
JavaScript(Turbo・Stimulus・モーダル等)が絡む挙動を確認したい◎
ボタンの色やピクセル単位の見た目を検証したい△(見た目専用ツールの領域)
1つのメソッドの戻り値だけを確認したい×(モデルスペックの担当)

なぜ「feature spec」ではなく「system spec」なのか

RSpec でブラウザテストを調べると、spec/features に置く feature spec と、spec/system に置く system spec の2つが出てきて混乱しますよね。結論から言うと、これから書くなら system spec 一択です。

feature spec は Rails がシステムテストを標準搭載する前からある、いわば「先祖」です。RSpec 公式も Rails 5.1 以降のアプリには system spec を推奨しています。system spec には feature spec に無い、次のうれしい機能がプリセットされています。

項目system specfeature spec(旧)
失敗時のスクショ自動(追加gem不要)手動設定(capybara-screenshot が必要)
DBの後片付け自動でロールバックdatabase_cleaner を自前設定しがち
ドライバ切り替えdriven_by でテストごとに簡単やや手間
ベースRails標準のシステムテストCapybara単体

ひとことで言えば、「system spec は Rails 標準のシステムテストを RSpec で書けるようにしたもの」。面倒な後片付けを Rails 側が肩代わりしてくれるので、こちらはテストの中身に集中できます。

セットアップ(gem を入れて初期化する)

まずは必要な gem を Gemfile に追加します。Rails 8 系の標準的な構成です。

group :development, :test do
  gem "rspec-rails", "~> 8.0"
end

group :test do
  gem "capybara", "~> 3.40"
  gem "selenium-webdriver"   # ブラウザを動かすドライバ
end

ここで1つ、以前のノウハウが古くなっているポイントがあります。かつては webdrivers gem で ChromeDriver を管理するのが定番でしたが、selenium-webdriver 4.11 以降は「Selenium Manager」が正しいドライバを自動でダウンロードするようになりました。Rails も新規アプリの Gemfile テンプレートから webdrivers を外しています。今から始めるなら webdrivers は不要です。

gem を入れたら、RSpec を初期化します。

bundle install
bin/rails generate rspec:install

これで spec/rails_helper.rb などが生成され、spec/system/ に置いたファイルが system spec として認識されるようになります。

ドライバを設定する(JSの有無で使い分ける)

system spec には「どのブラウザでテストを動かすか(=ドライバ)」の設定が要ります。ポイントは、JavaScript が要らないテストは速い rack_test、JS が絡むテストだけ本物のブラウザ(ヘッドレスChrome)を使うという使い分けです。

spec/support/capybara.rb を作り、次のように書きます(rails_helper.rb 内で spec/support を読み込む設定を有効にしておきます)。

RSpec.configure do |config|
  # 通常はブラウザを立ち上げない rack_test(速い)
  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  # js: true を付けたテストだけヘッドレスChromeで動かす
  config.before(:each, type: :system, js: true) do
    driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
  end
end

注意点として、RSpec の system spec は Rails の ApplicationSystemTestCase を読みません。デフォルトのままだと selenium が使われるので、上のように driven_by を自分で指定しておくのが安心です。

最初の1本を書いてみる(STEP1 → STEP2)

いよいよテスト本体です。いきなり複雑な例だと手が止まるので、まずは「ページが表示されること」だけの最小構成から始めます。

STEP1:ページが開けることを確認する

# spec/system/posts_spec.rb
require "rails_helper"

RSpec.describe "投稿機能", type: :system do
  it "投稿一覧ページが表示される" do
    visit posts_path
    expect(page).to have_content("投稿一覧")
  end
end

visit でページを開き、expect(page).to have_content(...) で「画面にこの文字があるはず」を検証する。これが system spec の基本形です。まずはこれが bundle exec rspec spec/system で緑になることを確認しましょう。

STEP2:ログインして投稿を作成する(実務の形)

次は本命。「ログイン → フォーム入力 → 送信 → 一覧に表示される」という、人が手でやっていた確認を丸ごと自動化します。テストデータの用意には FactoryBot を使う前提です。

# spec/system/posts_spec.rb
require "rails_helper"

RSpec.describe "投稿機能", type: :system do
  let(:user) { create(:user) }

  it "ログインして投稿を作成すると一覧に表示される", js: true do
    # ① ログイン(自作のログイン画面を操作)
    visit new_session_path
    fill_in "メールアドレス", with: user.email
    fill_in "パスワード", with: "password"
    click_on "ログイン"

    # ② 投稿を作成
    visit new_post_path
    fill_in "タイトル", with: "はじめての投稿"
    fill_in "本文", with: "system spec のテストです"
    click_on "投稿する"

    # ③ 結果を検証
    expect(page).to have_content("投稿を作成しました")
    expect(page).to have_content("はじめての投稿")
  end
end

やっていることは 「①データを用意 → ②visit・fill_in・click_on で操作 → ③have_content で検証」の3段構成だけ。この型さえ覚えれば、大半の画面テストは書けます。

system specの3段構成の図。①データを用意(create(:user))→②ブラウザを操作(visit・fill_in・click_on)→③結果を検証(have_content)
「①データ用意 → ②操作 → ③検証」の3段。この型に流し込めば大半の画面テストは書ける

よく使う Capybara メソッド一覧

system spec の操作は Capybara のメソッドで書きます。まずはこの表の分だけ押さえれば十分です。

やりたいことメソッド例
ページを開くvisitvisit posts_path
テキスト入力fill_infill_in "タイトル", with: "○"
ボタン/リンク押下click_onclick_on "投稿する"
チェックボックスcheck / uncheckcheck "規約に同意"
セレクトボックスselectselect "Rails", from: "カテゴリ"
文字があるか検証have_contentexpect(page).to have_content("○")
要素があるか検証have_selectorexpect(page).to have_selector(".post", count: 3)

テストが落ちる・効かないときのチェックリスト

system spec でつまずくポイントはだいたい決まっています。「なぜか落ちる」ときは、上から順に確認してみてください。

症状原因と対処
JSの動く画面(モーダル等)で要素が見つからないjs: true の付け忘れ。rack_test は JavaScript を実行しないので、Turbo や Stimulus が絡むテストには js: true を付ける
非同期の描画待ちで落ちる(Ajax後の要素)find や have_content は要素が現れるまで自動で待つ。この待機を使わず page.text.include?(...) のようにその瞬間の状態を直接見ると待たずに落ちる。判定は必ずマッチャ(have_content / have_no_content)で書く
ボタンを押した直後の検証が不安定スリープ(sleep 1)で待つのは悪手。Capybara の待機機構に任せ、have_content など「結果」を待つ表現で書く
データが次のテストに残るsystem spec はデフォルトでトランザクションを自動ロールバックする。残る場合は自前の database_cleaner 設定が競合していないか確認
ChromeDriver のバージョン不一致エラーselenium-webdriver 4.11+ なら Selenium Manager が自動解決する。古い webdrivers gem が残っていたら外す

Selenium と Cuprite、どっちを使う?

ヘッドレスChromeを動かすドライバには、定番の Selenium のほかに Cuprite という選択肢があります。Cuprite は CDP(Chrome DevTools Protocol)で直接 Chrome を操作するドライバで、Selenium より速く、ドライバの用意も不要です。

観点SeleniumCuprite
導入の手軽さ◎(Railsの実質標準)○(gem追加+数行の設定)
速度△◎(体感で速い)
デバッグ機能標準的◎(一時停止・ネットワーク待ち等)
情報量◎(枯れている)○(増加中)

Cuprite を使うなら、設定はこれだけです。

# Gemfile(group :test に追加)
gem "cuprite"

# spec/support/capybara.rb
require "capybara/cuprite"

Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(app, window_size: [1400, 1400], headless: true)
end

RSpec.configure do |config|
  config.before(:each, type: :system, js: true) do
    driven_by :cuprite
  end
end

迷ったら、まず Selenium で始めてOKです。テストが増えて実行時間が気になってきたら Cuprite に乗り換える、という順番が現実的です。最初から速度で悩まないことのほうが大事です。

CI(GitHub Actions)で system spec を回す

ローカルで緑になったら、CI でも自動実行して「壊れたら気づける」状態にしましょう。GitHub Actions の ubuntu-latest ランナーには Chrome がプリインストールされていて、Selenium Manager がドライバを解決するので、特別なインストールはほぼ不要です。

# .github/workflows/ci.yml(抜粋)
jobs:
  system_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - name: Prepare database
        run: bin/rails db:test:prepare
      - name: Run system specs
        run: bundle exec rspec spec/system

失敗時のスクリーンショットは tmp/screenshots/ に保存されます。CI では actions/upload-artifact でこのフォルダを保存しておくと、コケた画面をあとから確認できて調査が一気に楽になります。

どこまでテストを書くか(CTO視点の線引き)

最後に、実務でよく聞かれる「全部の画面に system spec を書くべき?」への答えです。結論は「No。壊れると痛い動線に絞る」です。system spec は本物のブラウザを動かすぶん実行が遅く、全網羅するとCIが重くなって誰もテストを回さなくなります。

おすすめは、①ログイン・決済・申込みなど「壊れると事業が止まる導線」 ②過去に本番でバグった画面の2つに集中投下すること。細かい分岐はモデルスペックやリクエストスペックに任せ、system spec は「一番大事な数本」を守る盾として使うのが、費用対効果の高い運用です。テストが落ちた原因の調べ方はエンジニアの「調べ方」の記事も参考にしてください。

まとめ

Rails のシステムテスト(system spec)は、「人がブラウザで手作業していた確認」をそのまま自動化する仕組みです。要点をまとめます。

  • これから書くなら feature spec ではなく system spec(スクショ自動・DB後片付け自動)
  • セットアップは rspec-rails + capybara + selenium-webdriver。webdrivers gem はもう不要
  • テストの型は 「①データ用意 → ②visit・fill_in・click_on → ③have_content で検証」の3段
  • JS が絡むテストにだけ js: true。速度が気になったら Cuprite へ
  • 全網羅せず、壊れると痛い導線に絞る

まずは STEP1 の「ページが開けるか」の1本から。緑になる体験ができれば、あとは型に流し込むだけです。認証まわりのテスト対象を用意したい方はDeviseのカスタマイズ記事、一覧・フォームの実装はRailsブログ機能の実装記事、画像アップロードを含む画面はActiveStorageアバター機能の記事も合わせてどうぞ。

スポンサーリンク

  • このエントリーをはてなブックマークに追加