testing-patterns について
RSpec と Capybara を使った Rails の自動テストガイドです。モデルやシステムテストの一貫した実践方法として、データ準備や要素選択のパターンを示します。
主な機能
- 01RSpec の規約とテスト構成
- 02FactoryBot によるデータ準備
- 03build を使ったバリデーションテスト
- 04Capybara のシステムテストパターン
- 05data-testid による安定した要素選択
活用シーン
- 01モデルのバリデーションテスト作成
- 02ユーザー向け Rails フローのテスト
- 03一覧テスト用レコードの事前作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rolemodel/ rolemodel-skills --skill testing-patternsインストール後、Claude Code で/testing-patternsを入力して読み込まれたか確認します。
詳細
テストパターン
概要
RSpec と Capybara を使用して自動テストを作成します。テストには Rails コンソールを使用したり、Rails サーバーを起動したりしないでください。
テストコマンド
bundle exec rspec
技術スタック
- RSpec - テストフレームワーク
- Capybara - システム/統合テスト
- FactoryBot - テストデータの生成
ベストプラクティス
一般的なガイドライン
- 実装の前、または実装と並行してテストを作成します
- コンソールを使った手動テストは避けます
- テストデータの作成にはファクトリーを使用します
- テストは目的を絞り、読みやすく保ちます
RSpec の規約
- 説明的な context ブロックと describe ブロックを使用します
- arrange-act-assert パターンに従います
- テストデータの設定には let を使用します
- インスタンス変数より
letを優先します
let と let!(遅延評価と即時評価)
let(遅延評価)を使用する場合:
- テスト内で変数を明示的に参照する
- 不要なデータベース書き込みを避けたい
- レコードの作成に副作用があり、それを制御したい
let!(即時評価)を使用する場合:
- テストの実行前にレコードがデータベースに存在している必要がある
- 変数を直接参照しないが、その存在が必要である
- レコードを検索するクエリ(インデックスページや検索機能など)をテストする
- 他のレコードが依存するバックグラウンドデータを設定する
例 - システムテスト:
RSpec.describe 'Job Management', type: :system do
# Use let! - these records must exist in DB for dropdowns and queries
let!(:location) { create(:location, name: 'Downtown Site') }
let!(:superintendent) { create(:user, :superintendent) }
# Use let - only created when explicitly referenced in a test
let(:job) { create(:job, name: 'Test Job', location:, superintendent:) }
it 'shows location in dropdown' do
visit new_job_path
# location must exist in DB for dropdown to display it
expect(page).to have_select('Location', with_options: [location.name])
end
it 'can delete a job' do
visit job_path(job) # job created here when first referenced
click_button 'Delete'
end
end
よくある落とし穴:
# ❌ WRONG - Test will fail because job2 doesn't exist in DB yet
let(:job1) { create(:job, name: 'Job 1') }
let(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # job1 created when referenced
expect(page).to have_content('Job 2') # FAIL - job2 never referenced, not in DB
end
# ✅ CORRECT - Both jobs exist before test runs
let!(:job1) { create(:job, name: 'Job 1') }
let!(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # Both jobs already in DB
expect(page).to have_content('Job 2') # Test passes
end
重要なポイント:
オブジェクト変数を明示的に操作せずにデータが表示されることを期待する場合(一覧の表示やドロップダウンからの選択など)は、レコードがデータベースに確実に作成されるよう let! を使用します。
バリデーションテストのパターン
無効なデータを指定した build を使用してバリデーションを明示的にテストし、モデルが無効であることを確認して、エラーメッセージを検証します。
describe 'validations' do
it 'must have a start date' do
membership = build(:membership, start_date: nil)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly "Start date can't be blank"
end
it 'enforces end date must follow start date' do
membership = build(:membership, start_date: 1.year.ago, end_date: 2.years.ago)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly 'End date must follow start date'
end
it 'permits empty end date' do
membership = build(:membership, start_date: 1.year.ago, end_date: nil)
expect(membership).to be_valid
end
end
重要なポイント:
- データベースへの書き込みを避けるため、
createではなくbuildを使用します - 無効なケースと有効なケースの両方をテストします
full_messagesを使ってエラーメッセージが完全に一致することを検証します- ビジネスルールがわかる説明的なテスト名を使用します
- 関連付けや列挙型はテストしないでください
Capybara のシステムテスト
- ユーザー向けの機能をテストします
- 要素を確実に選択するため、
dom_idを使ったdata-testid属性を使用します - 想定どおりのケースと境界条件をテストします
- テストが決定的に実行されるようにします
- sleep 文は避けます。要素の表示を待つネイティブの期待値など、Capybara の待機機能を使用します
- Stimulus コントローラーなどの JavaScript を実行するスペックでは、
:js(例:it 'does something', :js do)を使用します
data-testid による要素の選択
UI の変更に影響されにくい、安定して確実な要素選択には、dom_id を使った data-testid 属性を使用します。
ビュー:
tbody
- @entries.each do |entry|
tr data-testid=dom_id(entry)
td= entry.name
スペック:
within(data_test(entry1)) do
click_button 'Submit'
end
利点:
- テキストの変更(説明、ラベルなど)に強い
- 動的なコンテンツにも対応する
- テストの意図を自明にする
- ビューをリファクタリングしやすくなる
避けるもの:
- テキストに基づく検索:
within('tr', text: 'Entry 1') - スタイリング時に変更される可能性のある CSS クラスセレクター
- 過度に具体的な DOM のたどり方
within ブロックによるスコープ指定
同じテキストのボタンやリンクが複数あり、要素を特定できない場合は、within ブロックを使って操作対象を絞り込みます。
ベストプラクティス:
次の場合は必ず within ブロックを使用します。
- 複数の要素でテキストやラベルが重複している
- モーダル、パネル、オーバーレイを操作する
- 繰り返し表示される要素(テーブル行、カードなど)を扱う
- 「Ambiguous match」エラーでテストが失敗する
Turbo の確認ダイアログ
Turbo の確認ダイアログを表示するアクション(例:data: { turbo_confirm: 'message' } が設定された削除ボタン)をテストする場合は、用意されているヘルパーメソッドを使用します。
セットアップ:
ヘルパーファイルを作成します。
# spec/support/turbo_confirm_helper.rb
module TurboConfirmHelper
def accept_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-accept').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
def deny_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-cancel').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
end
RSpec の設定に追加します。
# spec/support/helpers.rb
RSpec.configure do |c|
# ...existing code...
c.include TurboConfirmHelper, type: :system
end
テストでの使用方法:
accept_turbo_confirm do
click_button 'Delete'
end
deny_turbo_confirm do
click_button 'Delete'
end
重要なポイント:
- Turbo の確認ダイアログを含むテストには、必ず
:jsタグを使用します - 確認を表示するアクションを、ヘルパーにブロックとして渡します
- ヘルパーはダイアログの表示と消失を自動的に待機します
- 「はい、実行します」をクリックするには
accept_turbo_confirmを使用します - 「キャンセル」をクリックするには
deny_turbo_confirmを使用します - ヘルパーには、信頼性を高めるための適切な待機時間とスコープ指定が含まれています
FactoryBot
- すべてのモデルに対してファクトリーを定義します
- バリエーションにはトレイトを使用します
- ファクトリーは最小限に保ちます
- 必要に応じて、テスト内で属性を上書きします
- モデルを直接インスタンス化せず、必ず
buildまたはcreateを使用します - データベースへの書き込みを避けるため、バリデーションテストには
buildを使用します - 永続化されたレコードが必要な場合は
createを使用します
今後のトピック
- モックとスタブのパターン
- テストの構成戦略
- パフォーマンステスト
- CI/CD との統合