mcpField
T
testing-patterns

testing-patterns

github.com/rolemodelテスト・品質737 回インストール今日更新
リポジトリ

testing-patterns について

RSpec と Capybara を使った Rails の自動テストガイドです。モデルやシステムテストの一貫した実践方法として、データ準備や要素選択のパターンを示します。

主な機能

  1. 01RSpec の規約とテスト構成
  2. 02FactoryBot によるデータ準備
  3. 03build を使ったバリデーションテスト
  4. 04Capybara のシステムテストパターン
  5. 05data-testid による安定した要素選択

活用シーン

  1. 01モデルのバリデーションテスト作成
  2. 02ユーザー向け Rails フローのテスト
  3. 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 ブロックを使用します。

  1. 複数の要素でテキストやラベルが重複している
  2. モーダル、パネル、オーバーレイを操作する
  3. 繰り返し表示される要素(テーブル行、カードなど)を扱う
  4. 「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 との統合

コメント