Skip to content

システムテスト詳細

システムテストは、Railsアプリケーションのエンドツーエンド(E2E)テストを実行するための機能です。Capybaraを使用して、実際のユーザーがブラウザで操作するのと同じように、アプリケーションの動作をテストできます。

⚠️ 「System Spec」 vs 「Request Spec」の境界線

Section titled “⚠️ 「System Spec」 vs 「Request Spec」の境界線”

Rails 5.1以降、統合テストはSystem Spec(Capybara使用)が推奨されていますが、すべてをSystem Specで書くとテスト実行時間が爆発します。

重要な理解: 「ブラウザの挙動(JSなど)」を確認する必要がないテストまでCapybaraで書くのは時間の無駄です。

問題点: APIの挙動や、単なる画面遷移の確認であれば、ブラウザを起動しないRequest Specの方が圧倒的に高速で安定しています。

# ❌ 非効率: System SpecでAPIの挙動を確認
RSpec.describe 'ユーザー一覧API', type: :system do
it 'ユーザー一覧を取得できること' do
visit '/api/v1/users'
# 問題: ブラウザを起動する必要がないのに、Capybaraを使っている
# 問題: テスト実行時間が遅い
end
end
# ✅ 効率的: Request SpecでAPIの挙動を確認
RSpec.describe 'ユーザー一覧API', type: :request do
it 'ユーザー一覧を取得できること' do
get '/api/v1/users'
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)).to be_an(Array)
# 高速: ブラウザを起動しないため、System Specより10倍以上速い
end
end

改善案: 「JSが絡む複雑なUI操作はSystem Spec」、「それ以外のエンドツーエンドの挙動確認はRequest Spec」という使い分けの基準を明文化しましょう。

テストの種類使用するSpec理由
JavaScriptを使ったUI操作System Specブラウザが必要
フォーム送信(JS不要)Request Spec高速で十分
APIの挙動確認Request Specブラウザ不要
画面遷移の確認Request Spec高速で十分
リアルタイム通信System SpecWebSocketのテストに必要

この使い分けにより、テスト実行時間を大幅に短縮できます。

システムテストは、以下のような特徴があります:

  • ユーザー視点のテスト: 実際のブラウザ操作をシミュレート
  • 統合テスト: モデル、コントローラー、ビュー、ルーティングなどが連携して動作することを確認
  • JavaScript対応: JavaScriptを使った動的な操作もテスト可能
  • データベースの使用: 実際のデータベースを使用してテスト(テスト用のデータベース)
spec/system/users_spec.rb
require 'rails_helper'
RSpec.describe 'Users', type: :system do
# letを使用してテストデータを準備
# 各テストケースで必要になったときに初めて実行される(遅延評価)
let(:user) { create(:user) }
describe 'ユーザー登録' do
it '新規ユーザーを登録できること' do
# ユーザー登録ページにアクセス
visit new_user_registration_path
# フォームに入力
fill_in 'メールアドレス', with: 'new@example.com'
fill_in 'パスワード', with: 'password123'
fill_in 'パスワード(確認)', with: 'password123'
# 登録ボタンをクリック
click_button '登録'
# 成功メッセージが表示されることを確認
expect(page).to have_content('アカウント登録が完了しました')
# データベースに正しく保存されたことを確認
expect(User.last.email).to eq('new@example.com')
end
it 'バリデーションエラーが表示されること' do
visit new_user_registration_path
# 必須項目を入力せずに送信
click_button '登録'
# エラーメッセージが表示されることを確認
expect(page).to have_content('メールアドレスを入力してください')
expect(page).to have_content('パスワードを入力してください')
end
end
describe 'ログイン' do
it 'ログインできること' do
# ログインページにアクセス
visit new_user_session_path
# ログイン情報を入力
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: user.password
# ログインボタンをクリック
click_button 'ログイン'
# ログイン成功メッセージが表示されることを確認
expect(page).to have_content('ログインしました')
# ログイン後のページに遷移したことを確認
expect(page).to have_current_path(root_path)
end
it '無効な認証情報ではログインできないこと' do
visit new_user_session_path
fill_in 'メールアドレス', with: 'invalid@example.com'
fill_in 'パスワード', with: 'wrongpassword'
click_button 'ログイン'
# エラーメッセージが表示されることを確認
expect(page).to have_content('メールアドレスまたはパスワードが違います')
end
end
describe 'JavaScriptを使った操作' do
# js: trueを指定することで、JavaScriptドライバー(Selenium)を使用
it 'モーダルを開閉できること', js: true do
visit root_path
# モーダルを開くボタンをクリック
click_button 'モーダルを開く'
# モーダルが表示されることを確認
# visible: trueで、実際に表示されている要素のみを検証
expect(page).to have_css('.modal', visible: true)
# モーダル内のコンテンツが表示されることを確認
within '.modal' do
expect(page).to have_content('モーダルの内容')
end
# 閉じるボタンをクリック
click_button '閉じる'
# モーダルが非表示になることを確認
expect(page).not_to have_css('.modal', visible: true)
end
it 'Ajaxでデータを取得できること', js: true do
visit users_path
# Ajaxリクエストをトリガーするボタンをクリック
click_button '更新'
# Ajaxの完了を待ってから検証
# waitオプションで最大待機時間を指定(デフォルトは2秒)
expect(page).to have_content('更新が完了しました', wait: 5)
# 更新されたデータが表示されることを確認
expect(page).to have_css('.user-list .user-item', count: 10)
end
end
describe 'ページネーション' do
before do
# テストデータを準備(30件のユーザーを作成)
create_list(:user, 30)
end
it 'ページネーションが動作すること' do
visit users_path
# 1ページ目に10件表示されることを確認
expect(page).to have_css('.user-item', count: 10)
# 2ページ目に遷移
click_link '次へ'
# 2ページ目に10件表示されることを確認
expect(page).to have_css('.user-item', count: 10)
expect(page).to have_current_path(users_path(page: 2))
end
end
end

Capybaraは、異なるドライバーを使用してブラウザを操作できます。各ドライバーの特徴と設定方法を説明します。

Rack::Testドライバー(デフォルト)

Section titled “Rack::Testドライバー(デフォルト)”
spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each, type: :system) do
# デフォルトはRack::Test(高速、JavaScript非対応)
driven_by :rack_test
end
end

特徴:

  • 高速: JavaScriptを実行しないため、非常に高速
  • 軽量: ブラウザを起動しないため、リソース消費が少ない
  • 制限: JavaScriptを使った動的な操作はテストできない

使用例:

# JavaScriptを使わない基本的なフォーム送信など
RSpec.describe 'ユーザー登録', type: :system do
it 'フォームからユーザーを登録できること' do
visit new_user_registration_path
fill_in 'メールアドレス', with: 'test@example.com'
fill_in 'パスワード', with: 'password123'
click_button '登録'
expect(page).to have_content('登録が完了しました')
end
end

Selenium Chrome Headlessドライバー(推奨)

Section titled “Selenium Chrome Headlessドライバー(推奨)”
spec/support/capybara.rb
require 'capybara/rails'
require 'selenium-webdriver'
Capybara.register_driver :selenium_chrome_headless do |app|
options = Selenium::WebDriver::Chrome::Options.new
# ヘッドレスモード(ブラウザのGUIを表示しない)
options.add_argument('--headless')
# CI環境での実行に必要(セキュリティサンドボックスを無効化)
options.add_argument('--no-sandbox')
# メモリ不足エラーを防ぐ
options.add_argument('--disable-dev-shm-usage')
# GPUを無効化(ヘッドレスモードでは不要)
options.add_argument('--disable-gpu')
# ウィンドウサイズを指定(レスポンシブデザインのテストに重要)
options.add_argument('--window-size=1920,1080')
# ログレベルの設定
options.add_argument('--log-level=3')
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end
# JavaScriptが必要なテストで使用するドライバーを設定
Capybara.javascript_driver = :selenium_chrome_headless
# デフォルトの待機時間を設定(秒)
Capybara.default_max_wait_time = 5

特徴:

  • JavaScript対応: JavaScriptを使った動的な操作もテスト可能
  • 高速: ヘッドレスモードで実行されるため、通常のSeleniumより高速
  • CI/CD対応: サーバー環境でも実行可能
  • 推奨: 多くのプロジェクトで推奨される設定

使用例:

spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each, type: :system, js: true) do
# JavaScriptが必要な場合はSeleniumを使用
driven_by :selenium_chrome_headless
end
end
# テストコード
RSpec.describe 'Ajax通信', type: :system, js: true do
it 'Ajaxでデータを取得できること' do
visit users_path
click_button '更新'
# Ajaxの完了を待つ(自動的に待機)
expect(page).to have_content('更新が完了しました', wait: 5)
end
end

⚠️ 「何でも待つ」ことの副作用

Section titled “⚠️ 「何でも待つ」ことの副作用”

wait: 5などの待機処理は重要ですが、これに頼りすぎるとテストが不安定(Flaky)になります。

重要な理解: 「要素が表示されるまで待つ」のは正解ですが、「要素が消えるまで待つ」処理を忘れると、次のテストケースに影響が出ます。

問題点: 非同期処理(Ajaxなど)の結果を待たずに次の操作に移ると、前のテストのデータが残っていてテストが落ちる「謎の失敗」に悩まされることになります。

# ❌ 危険: 要素が消えるまで待たない
it '投稿を削除できること', js: true do
visit posts_path
click_button '削除'
# 問題: 削除処理が完了する前に次のテストに進む
# 問題: 次のテストで削除された投稿がまだ表示される
end
# ✅ 安全: 要素が消えるまで待つ
it '投稿を削除できること', js: true do
visit posts_path
expect(page).to have_css('.post-item', count: 5)
click_button '削除'
# 削除処理の完了を待つ
expect(page).not_to have_css('.post-item', count: 5, wait: 5)
expect(page).to have_css('.post-item', count: 4)
# または、削除された要素が消えるまで待つ
expect(page).not_to have_css('.post-item', text: '削除された投稿', wait: 5)
end

改善案: 破壊的な操作(削除など)の後は、必ずその要素がnot_to have_cssになることを確認してから次のステップに進む、というルールを徹底しましょう。

# ✅ 良い例: 非同期処理の完了を確実に待つ
it 'Ajaxでデータを更新できること', js: true do
visit users_path
# 更新前の状態を確認
expect(page).to have_content('古いデータ')
click_button '更新'
# 更新処理の完了を待つ(2つの方法)
# 方法1: 新しいデータが表示されるまで待つ
expect(page).to have_content('新しいデータ', wait: 5)
# 方法2: 古いデータが消えるまで待つ
expect(page).not_to have_content('古いデータ', wait: 5)
# 両方を確認することで、より確実に待機できる
end

このルールにより、テストの安定性が大幅に向上します。

Selenium Chromeドライバー(デバッグ用)

Section titled “Selenium Chromeドライバー(デバッグ用)”
spec/support/capybara.rb
Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome)
end

特徴:

  • デバッグに便利: 実際のブラウザが起動するため、テストの動作を確認できる
  • 開発環境向け: ローカル開発環境でのデバッグに最適
  • 遅い: ブラウザを起動するため、テスト実行が遅い

使用例:

# デバッグ時のみ使用
RSpec.describe '複雑な操作', type: :system, js: true do
before do
# デバッグ時のみ通常のChromeを使用
driven_by :selenium_chrome if ENV['DEBUG']
end
it '複雑な操作をテストすること' do
# テストコード
end
end
# ❌ 問題: 要素が見つからない
click_button '送信' # Capybara::ElementNotFound
# ✅ 解決: 待機時間を設定
Capybara.default_max_wait_time = 5
# または、特定の要素に対して待機
expect(page).to have_button('送信', wait: 10)
click_button '送信'
# ❌ 問題: JavaScriptの実行が完了する前に検証
click_button 'Ajax送信'
expect(page).to have_content('完了') # まだ実行されていない可能性
# ✅ 解決: マッチャーは自動的に待機するが、明示的に待機時間を指定
expect(page).to have_content('完了', wait: 5)
# または、特定の要素の出現を待つ
expect(page).to have_css('.success-message', visible: true, wait: 5)

3. データベースのクリーンアップ

Section titled “3. データベースのクリーンアップ”
spec/rails_helper.rb
RSpec.configure do |config|
# 各テストの前にデータベースをクリーンアップ
config.before(:each, type: :system) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.start
end
config.after(:each, type: :system) do
DatabaseCleaner.clean
end
end
# ✅ 良い例: 各テストが独立している
RSpec.describe 'ユーザー登録', type: :system do
it '新規ユーザーを登録できること' do
visit new_user_registration_path
fill_in 'メールアドレス', with: 'new@example.com'
fill_in 'パスワード', with: 'password123'
click_button '登録'
expect(page).to have_content('登録が完了しました')
expect(User.count).to eq(1)
end
it '既存のメールアドレスでは登録できないこと' do
create(:user, email: 'existing@example.com')
visit new_user_registration_path
fill_in 'メールアドレス', with: 'existing@example.com'
fill_in 'パスワード', with: 'password123'
click_button '登録'
expect(page).to have_content('メールアドレスは既に使用されています')
end
end
spec/support/system_test_helpers.rb
module SystemTestHelpers
def login_as(user)
visit new_user_session_path
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: user.password
click_button 'ログイン'
end
def logout
click_link 'ログアウト'
end
def fill_user_form(user_attributes = {})
fill_in 'ユーザー名', with: user_attributes[:name] || 'テストユーザー'
fill_in 'メールアドレス', with: user_attributes[:email] || 'test@example.com'
fill_in 'パスワード', with: user_attributes[:password] || 'password123'
end
end
RSpec.configure do |config|
config.include SystemTestHelpers, type: :system
end
# 使用例
RSpec.describe 'ユーザー機能', type: :system do
let(:user) { create(:user) }
before do
login_as(user)
end
it 'プロフィールを編集できること' do
visit edit_user_path(user)
fill_user_form(name: '新しい名前')
click_button '更新'
expect(page).to have_content('更新が完了しました')
end
end

3. ページオブジェクトパターンの使用

Section titled “3. ページオブジェクトパターンの使用”
spec/support/pages/user_registration_page.rb
class UserRegistrationPage
include Capybara::DSL
def visit_page
visit new_user_registration_path
end
def fill_email(email)
fill_in 'メールアドレス', with: email
end
def fill_password(password)
fill_in 'パスワード', with: password
end
def fill_password_confirmation(password)
fill_in 'パスワード(確認)', with: password
end
def submit
click_button '登録'
end
def success_message
page.find('.success-message').text
end
def error_messages
page.all('.error-message').map(&:text)
end
end
# spec/system/users_spec.rb
RSpec.describe 'ユーザー登録', type: :system do
let(:page) { UserRegistrationPage.new }
it '新規ユーザーを登録できること' do
page.visit_page
page.fill_email('test@example.com')
page.fill_password('password123')
page.fill_password_confirmation('password123')
page.submit
expect(page.success_message).to eq('登録が完了しました')
end
end

🛠️ database.ymlのテスト設定(追加まさかり)

Section titled “🛠️ database.ymlのテスト設定(追加まさかり)”

テストの実行速度を上げるために、database.ymlで見落とされがちな設定があります。

重要な理解: Rails 6から導入されたParallel Testing(テストの並列実行)を使う場合、CPUコア数分だけDB接続が必要になります。pool数が少ないと、並列実行中にConnectionTimeoutErrorでテストが全滅します。

config/database.yml
test:
<<: *default
database: my_app_test
# まさかり:テスト並列実行(parallelize)時のプール数不足
# Rails 6以降の並列テストを使う場合、テストワーカーの数だけ接続が必要
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i * 2 %>
checkout_timeout: 5
reaping_frequency: 10

重要ポイント:

  • 並列テスト: parallelize(workers: 4)を使う場合、4つのワーカーが同時にDBに接続する
  • Poolサイズ: ワーカー数 × スレッド数 + 余裕を持たせた設定が必要
  • デフォルトの5では不足: 並列実行時にConnectionTimeoutErrorが発生する

この設定により、並列テスト実行時の接続エラーを防げます。

🛠️ CircleCI / GitHub Actionsでの「スクリーンショット」

Section titled “🛠️ CircleCI / GitHub Actionsでの「スクリーンショット」”

CI(自動テスト環境)でSystem Specが落ちた時、原因の特定は困難です。

重要な理解: 「CIでだけテストが落ちる」という怪現象への対策がありません。

解決策: RailsのSystem Test標準機能であるtake_failed_screenshotを活用しましょう。失敗時のブラウザの状態を画像で保存するように設定しておけば、デバッグ時間が1/10になります。

spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each, type: :system) do
# 失敗時にスクリーンショットを自動保存
take_failed_screenshot
end
end
# または、より詳細な設定
RSpec.configure do |config|
config.after(:each, type: :system) do |example|
if example.exception
# 失敗時にスクリーンショットとHTMLを保存
take_screenshot
save_page # HTMLも保存
end
end
end

保存先:

  • スクリーンショット: tmp/screenshots/
  • HTML: tmp/capybara/

CI環境での設定:

.github/workflows/test.yml
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v2
with:
name: screenshots
path: tmp/screenshots/

この設定により、CIでのテスト失敗時の原因特定が容易になります。

システムテストは、RailsアプリケーションのE2Eテストを実行するための強力な機能です。以下のポイントを押さえることで、効果的なテストを書くことができます:

  • ドライバーの使い分け: JavaScriptが必要な場合のみSeleniumを使用し、それ以外はRack::Testを使用
  • System Spec vs Request Spec: JSが絡む複雑なUI操作はSystem Spec、それ以外はRequest Spec
  • 待機処理: 動的なコンテンツに対しては適切な待機時間を設定し、要素が消えるまで待つ
  • テストの独立性: 各テストが独立して実行できるようにする
  • ヘルパーメソッド: 繰り返し使用する操作はヘルパーメソッドに抽出
  • ページオブジェクトパターン: 複雑なテストはページオブジェクトパターンで整理
  • database.ymlの設定: 並列テスト実行時のpool数を適切に設定
  • スクリーンショット: CI環境での失敗時にスクリーンショットを自動保存

これらのベストプラクティスを守ることで、保守性が高く、実行速度の速い、信頼性の高いテストスイートを構築できます。

🚀 設計レビューでの「まさかり」文例

Section titled “🚀 設計レビューでの「まさかり」文例”

実務でのコードレビューで使用できる、具体的な指摘文例を以下に示します。

テストの粒度が粗すぎる場合の指摘

Section titled “テストの粒度が粗すぎる場合の指摘”
【指摘】テストの粒度が粗すぎます。
【問題】1つのitブロックの中で、「ログインして、プロフィールを編集して、パスワードを変えて、ログアウトする」という一連の流れをすべてテストしています。
【影響】どこかで失敗した際、原因の切り分けが難しく、またテストコードの可読性が著しく低下します。
【推奨】1つのitブロックでは「1つの期待される振る舞い」のみを検証するように分割してください。

System SpecとRequest Specの使い分けが不適切な場合の指摘

Section titled “System SpecとRequest Specの使い分けが不適切な場合の指摘”
【指摘】System SpecとRequest Specの使い分けが不適切です。
【問題】APIの挙動確認にSystem Specを使用しており、テスト実行時間が無駄に長くなっています。
【影響】テストスイート全体の実行時間が増加し、開発効率が低下します。
【推奨】JavaScriptが絡まないAPIの挙動確認はRequest Specを使用してください。

待機処理が不十分な場合の指摘

Section titled “待機処理が不十分な場合の指摘”
【指摘】非同期処理の完了を待たずに次の操作に移っています。
【問題】削除操作の後、要素が消えるまで待たずに次のテストに進んでいます。
【影響】テストが不安定(Flaky)になり、CI環境でランダムに失敗する可能性があります。
【推奨】破壊的な操作の後は、必ずその要素がnot_to have_cssになることを確認してから次のステップに進んでください。

これらの指摘文例を参考に、コードレビューで適切なフィードバックを行い、堅牢なテストスイートを構築しましょう。