システムテスト詳細
システムテスト詳細
Section titled “システムテスト詳細”システムテストは、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を使っている # 問題: テスト実行時間が遅い endend
# ✅ 効率的: 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倍以上速い endend改善案: 「JSが絡む複雑なUI操作はSystem Spec」、「それ以外のエンドツーエンドの挙動確認はRequest Spec」という使い分けの基準を明文化しましょう。
| テストの種類 | 使用するSpec | 理由 |
|---|---|---|
| JavaScriptを使ったUI操作 | System Spec | ブラウザが必要 |
| フォーム送信(JS不要) | Request Spec | 高速で十分 |
| APIの挙動確認 | Request Spec | ブラウザ不要 |
| 画面遷移の確認 | Request Spec | 高速で十分 |
| リアルタイム通信 | System Spec | WebSocketのテストに必要 |
この使い分けにより、テスト実行時間を大幅に短縮できます。
システムテストとは
Section titled “システムテストとは”システムテストは、以下のような特徴があります:
- ユーザー視点のテスト: 実際のブラウザ操作をシミュレート
- 統合テスト: モデル、コントローラー、ビュー、ルーティングなどが連携して動作することを確認
- JavaScript対応: JavaScriptを使った動的な操作もテスト可能
- データベースの使用: 実際のデータベースを使用してテスト(テスト用のデータベース)
Capybaraを使ったE2Eテスト
Section titled “Capybaraを使ったE2Eテスト”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 endendドライバーの設定
Section titled “ドライバーの設定”Capybaraは、異なるドライバーを使用してブラウザを操作できます。各ドライバーの特徴と設定方法を説明します。
Rack::Testドライバー(デフォルト)
Section titled “Rack::Testドライバー(デフォルト)”RSpec.configure do |config| config.before(:each, type: :system) do # デフォルトはRack::Test(高速、JavaScript非対応) driven_by :rack_test endend特徴:
- 高速: 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('登録が完了しました') endendSelenium Chrome Headlessドライバー(推奨)
Section titled “Selenium Chrome Headlessドライバー(推奨)”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対応: サーバー環境でも実行可能
- 推奨: 多くのプロジェクトで推奨される設定
使用例:
RSpec.configure do |config| config.before(:each, type: :system, js: true) do # JavaScriptが必要な場合はSeleniumを使用 driven_by :selenium_chrome_headless endend
# テストコードRSpec.describe 'Ajax通信', type: :system, js: true do it 'Ajaxでデータを取得できること' do visit users_path
click_button '更新'
# Ajaxの完了を待つ(自動的に待機) expect(page).to have_content('更新が完了しました', wait: 5) endend⚠️ 「何でも待つ」ことの副作用
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ドライバー(デバッグ用)”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 # テストコード endendよくある問題と解決方法
Section titled “よくある問題と解決方法”1. 要素が見つからないエラー
Section titled “1. 要素が見つからないエラー”# ❌ 問題: 要素が見つからないclick_button '送信' # Capybara::ElementNotFound
# ✅ 解決: 待機時間を設定Capybara.default_max_wait_time = 5
# または、特定の要素に対して待機expect(page).to have_button('送信', wait: 10)click_button '送信'2. JavaScriptの実行待ち
Section titled “2. JavaScriptの実行待ち”# ❌ 問題: 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. データベースのクリーンアップ”RSpec.configure do |config| # 各テストの前にデータベースをクリーンアップ config.before(:each, type: :system) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.start end
config.after(:each, type: :system) do DatabaseCleaner.clean endendベストプラクティス
Section titled “ベストプラクティス”1. テストの独立性
Section titled “1. テストの独立性”# ✅ 良い例: 各テストが独立している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('メールアドレスは既に使用されています') endend2. ヘルパーメソッドの活用
Section titled “2. ヘルパーメソッドの活用”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' endend
RSpec.configure do |config| config.include SystemTestHelpers, type: :systemend
# 使用例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('更新が完了しました') endend3. ページオブジェクトパターンの使用
Section titled “3. ページオブジェクトパターンの使用”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) endend
# spec/system/users_spec.rbRSpec.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('登録が完了しました') endend🛠️ database.ymlのテスト設定(追加まさかり)
Section titled “🛠️ database.ymlのテスト設定(追加まさかり)”テストの実行速度を上げるために、database.ymlで見落とされがちな設定があります。
重要な理解:
Rails 6から導入されたParallel Testing(テストの並列実行)を使う場合、CPUコア数分だけDB接続が必要になります。pool数が少ないと、並列実行中にConnectionTimeoutErrorでテストが全滅します。
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になります。
RSpec.configure do |config| config.before(:each, type: :system) do # 失敗時にスクリーンショットを自動保存 take_failed_screenshot endend
# または、より詳細な設定RSpec.configure do |config| config.after(:each, type: :system) do |example| if example.exception # 失敗時にスクリーンショットとHTMLを保存 take_screenshot save_page # HTMLも保存 end endend保存先:
- スクリーンショット:
tmp/screenshots/ - HTML:
tmp/capybara/
CI環境での設定:
- 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になることを確認してから次のステップに進んでください。これらの指摘文例を参考に、コードレビューで適切なフィードバックを行い、堅牢なテストスイートを構築しましょう。