RSpec on Railsの基礎知識
RSpecとは何か?Railsプロジェクトでテストが重要な理由
RSpecは、Ruby用の振る舞い駆動開発(BDD)のためのテスティングフレームワークです。人間が読みやすい形式でテストを記述できることが特徴で、以下のような直感的な文法を提供します:
describe User do
it "フルネームを正しく結合できること" do
user = User.new(first_name: "太郎", last_name: "山田")
expect(user.full_name).to eq "山田 太郎"
end
end
Railsプロジェクトでテストが重要な理由は以下の3点です:
- 品質保証と回帰バグの防止
- 新機能追加や既存機能の修正時に、意図しない副作用を早期発見
- 本番環境でのクリティカルな不具合を事前に防止
- リファクタリング時の安全性確保
- 開発速度の向上
- 手動テストの工数削減
- バグの早期発見による修正コストの低減
- 設計の改善点の早期発見
- ドキュメントとしての役割
- テストコードが仕様書として機能
- 新規メンバーの学習コスト削減
- コードの意図や制約の明確化
RSpecがRailsで選ばれる3つの理由
- 豊富なテストヘルパーとマッチャー
# HTTPリクエストのテストが簡単
describe "GET /users" do
it "ユーザー一覧を取得できること" do
get users_path
expect(response).to have_http_status(:success)
expect(response.body).to include("ユーザー一覧")
end
end
# モデルのバリデーションテストも直感的
describe User do
it { should validate_presence_of(:email) }
it { should have_many(:posts) }
end
- 柔軟なテストの構造化
RSpec.describe User do
context "管理者の場合" do
let(:user) { create(:user, :admin) }
it "システム設定にアクセスできること" do
expect(user.can_access_system_settings?).to be true
end
end
context "一般ユーザーの場合" do
let(:user) { create(:user) }
it "システム設定にアクセスできないこと" do
expect(user.can_access_system_settings?).to be false
end
end
end
- 充実したエコシステム
- factory_bot: テストデータの作成を効率化
- faker: ダミーデータの生成
- database_cleaner: テスト用DBの管理
- shoulda-matchers: よく使うマッチャーの提供
RSpecとMinitest(Rails標準テスト)の違い
以下の表で主な違いを比較します:
| 項目 | RSpec | Minitest |
|---|---|---|
| 文法スタイル | BDD形式(describe/it) | TDD形式(class/test) |
| 学習コスト | やや高い(多機能) | 低い(シンプル) |
| 機能の豊富さ | 非常に豊富 | 必要最小限 |
| カスタマイズ性 | 高い | 中程度 |
| 実行速度 | 若干遅い | 速い |
| エコシステム | 非常に充実 | 基本的 |
具体的な記述の違い:
# RSpecの場合
RSpec.describe Calculator do
describe "#add" do
it "returns the sum of two numbers" do
calculator = Calculator.new
expect(calculator.add(1, 2)).to eq(3)
end
end
end
# Minitestの場合
class CalculatorTest < Minitest::Test
def test_add_returns_sum_of_two_numbers
calculator = Calculator.new
assert_equal 3, calculator.add(1, 2)
end
end
RSpecは豊富な機能と表現力の高さで多くの開発者に選ばれていますが、プロジェクトの規模や要件に応じて適切なテスティングフレームワークを選択することが重要です。小規模なプロジェクトではMinitestの方が適している場合もあります。
RSpec環境構築の完全ガイド
Gemfileの設定とインストール手順
RSpecをRailsプロジェクトに導入する手順を詳しく解説します。
- 必要なgemの追加
group :development, :test do gem 'rspec-rails' # RSpec本体 gem 'factory_bot_rails' # テストデータ作成 gem 'faker' # ダミーデータ生成 gem 'database_cleaner' # テストDB管理 gem 'shoulda-matchers' # 追加のマッチャー gem 'capybara' # システムスペック用 gem 'webdrivers' # ブラウザドライバ管理 end
- インストールコマンドの実行
# gemのインストール bundle install # RSpecの初期設定 rails generate rspec:install
これにより以下のファイルが生成されます:
.rspec: RSpec実行時の基本オプションspec/spec_helper.rb: RSpec全体の設定spec/rails_helper.rb: Rails特有の設定
最適な設定ファイル(spec_helper.rb)の書き方
以下に、推奨されるspec/rails_helper.rbの設定を示します:
require 'spec_helper'
require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'
require 'capybara/rspec'
require 'shoulda/matchers'
# テストデータベースの設定
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
# Factory Botの設定
config.include FactoryBot::Syntax::Methods
# データベースクリーニング戦略
config.use_transactional_fixtures = true
# システムスペックの設定
config.before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
# 不要なwarningを非表示
config.warnings = false
# テスト失敗時に詳細な情報を表示
config.full_backtrace = false
# フォーカスされたテストのみ実行
config.filter_run_when_matching :focus
# テストの順序をランダム化
config.order = :random
end
# Shoulda Matchersの設定
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
Factory BotとMockライブラリの導入
- Factory Botの基本設定
# spec/support/factory_bot.rb
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { "password123" }
name { Faker::Name.name }
# 管理者ユーザーのトレイト
trait :admin do
admin { true }
end
end
end
- 効果的なモックの設定
# spec/support/mocks.rb
RSpec.configure do |config|
# モックの基本設定
config.mock_with :rspec do |mocks|
# 存在しないメソッドのモックを禁止
mocks.verify_partial_doubles = true
# モックされていないメソッドの呼び出しを許可
mocks.verify_doubled_constant_names = true
end
end
# モックの使用例
RSpec.describe UserMailer do
describe '#welcome_email' do
it 'メール送信が正しく行われること' do
user = create(:user)
mailer = double("Mailer")
allow(mailer).to receive(:deliver_now)
expect(UserMailer.welcome_email(user)).to be_delivered
end
end
end
- テストデータのクリーニング設定
# spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
これらの設定により、以下のような利点が得られます:
- 効率的なテストデータの作成と管理
- 安定したテスト環境の実現
- テストの実行速度の最適化
- メンテナンス性の高いテストコードの作成
また、以下の点に注意して環境を構築することをお勧めします:
- テストの実行速度を考慮した設定
- CIツールとの互換性の確保
- チーム全体での一貫性のある設定の維持
- セキュリティに関する設定の適切な管理
モデルスペックの実践テクニック
バリデーションのテストパターン集
モデルのバリデーションテストは、データの整合性を保証する重要な要素です。以下に主要なパターンを示します。
- 基本的なバリデーションテスト
RSpec.describe User, type: :model do
describe 'バリデーション' do
# 必須項目のテスト
it { should validate_presence_of(:email) }
it { should validate_presence_of(:username) }
# 文字数制限のテスト
it { should validate_length_of(:password).is_at_least(8) }
it { should validate_length_of(:username).is_at_most(50) }
# ユニーク制約のテスト
it { should validate_uniqueness_of(:email).case_insensitive }
# フォーマットのテスト
it { should allow_value('user@example.com').for(:email) }
it { should_not allow_value('invalid_email').for(:email) }
end
# カスタムバリデーションのテスト
describe '#validate_age_requirement' do
context '18歳以上の場合' do
let(:user) { build(:user, birth_date: 20.years.ago) }
it '有効であること' do
expect(user).to be_valid
end
end
context '18歳未満の場合' do
let(:user) { build(:user, birth_date: 17.years.ago) }
it '無効であること' do
expect(user).not_to be_valid
expect(user.errors[:birth_date]).to include('は18歳以上である必要があります')
end
end
end
end
コールバックとスコープのテスト方法
- コールバックのテスト
RSpec.describe Post, type: :model do
describe 'コールバック' do
describe 'before_save' do
it 'スラッグを自動生成すること' do
post = build(:post, title: '新しい記事のタイトル')
post.save
expect(post.slug).to eq 'shin-shii-ji-shi-notaitoru'
end
end
describe 'after_create' do
it '作成後に通知が送信されること' do
user = create(:user)
expect {
create(:post, user: user)
}.to change(Notification, :count).by(1)
end
end
describe 'before_destroy' do
it '関連するコメントも削除されること' do
post = create(:post)
create_list(:comment, 3, post: post)
expect {
post.destroy
}.to change(Comment, :count).by(-3)
end
end
end
end
- スコープのテスト
RSpec.describe Article, type: :model do
describe 'スコープ' do
# published スコープのテスト
describe '.published' do
let!(:published_article) { create(:article, :published) }
let!(:draft_article) { create(:article, :draft) }
it '公開済みの記事のみを返すこと' do
expect(Article.published).to include(published_article)
expect(Article.published).not_to include(draft_article)
end
end
# recent スコープのテスト
describe '.recent' do
let!(:old_article) { create(:article, created_at: 1.week.ago) }
let!(:new_article) { create(:article, created_at: 1.hour.ago) }
it '作成日時の降順で記事を返すこと' do
expect(Article.recent.first).to eq new_article
expect(Article.recent.last).to eq old_article
end
end
# by_category スコープのテスト
describe '.by_category' do
let(:category) { create(:category) }
let!(:article_in_category) { create(:article, category: category) }
let!(:article_in_other_category) { create(:article) }
it '指定したカテゴリーの記事のみを返すこと' do
expect(Article.by_category(category.id)).to include(article_in_category)
expect(Article.by_category(category.id)).not_to include(article_in_other_category)
end
end
end
end
アソシエーションの行動を確実にテストする
- 基本的なアソシエーションテスト
RSpec.describe User, type: :model do
describe 'アソシエーション' do
# 関連付けの存在確認
it { should have_many(:posts).dependent(:destroy) }
it { should have_many(:comments).through(:posts) }
it { should belong_to(:team).optional }
# has_many関連のテスト
describe 'posts関連' do
let(:user) { create(:user) }
it '投稿を追加できること' do
expect {
user.posts.create(attributes_for(:post))
}.to change(Post, :count).by(1)
end
it '投稿を削除できること' do
post = create(:post, user: user)
expect {
user.posts.destroy(post)
}.to change(Post, :count).by(-1)
end
end
end
# ポリモーフィック関連のテスト
describe 'polymorphic associations' do
it { should have_many(:images).as(:imageable) }
it 'イメージを追加できること' do
user = create(:user)
expect {
user.images.create(attributes_for(:image))
}.to change(Image, :count).by(1)
end
end
# 相互関連のテスト
describe 'mutual associations' do
let(:user) { create(:user) }
let(:followed_user) { create(:user) }
it 'ユーザーをフォローできること' do
expect {
user.followed_users << followed_user
}.to change(user.followed_users, :count).by(1)
end
it 'フォロワーを取得できること' do
user.followed_users << followed_user
expect(followed_user.followers).to include(user)
end
end
end
モデルスペックを書く際の重要なポイント:
- テストの独立性を保つ
- 各テストは他のテストに依存せず実行できること
- テストデータは各テストで適切に準備すること
- テストの可読性を高める
- describeとcontextを適切に使用する
- テストの意図が明確になる命名を心がける
- テストの保守性を考慮
- DRYなテストコードを心がける
- shared_examplesを活用する
- ファクトリを効率的に利用する
- エッジケースのテスト
- 異常系のテストを忘れずに実装
- 境界値のテストを含める
- 特殊なケースの動作を確認する
コントローラスペックの書き方マスター
リクエストスペックとの使い分け
コントローラスペックとリクエストスペックの適切な使い分けは、効果的なテスト戦略の鍵となります。
- コントローラスペックの特徴
RSpec.describe UsersController, type: :controller do
describe 'GET #index' do
it 'ユーザー一覧を取得すること' do
get :index
expect(assigns(:users)).to match_array(User.all)
expect(response).to render_template :index
end
end
end
- リクエストスペックの特徴
RSpec.describe 'Users', type: :request do
describe 'GET /users' do
it 'ユーザー一覧ページが表示されること' do
get users_path
expect(response).to have_http_status(200)
expect(response.body).to include('ユーザー一覧')
end
end
end
使い分けの指針:
| テストの種類 | 使用ケース | 利点 |
|---|---|---|
| コントローラスペック | ・内部ロジックのテスト ・アサインされた変数の確認 ・細かい条件分岐のテスト | ・より詳細なテストが可能 ・実行が高速 ・モックが使いやすい |
| リクエストスペック | ・エンドツーエンドのテスト ・APIのテスト ・実際のHTTPリクエストの確認 | ・より実践的なテスト ・実際の挙動に近い ・APIテストに適している |
パラメータ処理と戻り値のテスト
- 基本的なCRUDアクションのテスト
RSpec.describe ArticlesController, type: :controller do
describe 'POST #create' do
context '有効なパラメータの場合' do
let(:valid_params) { { article: attributes_for(:article) } }
it '記事が作成されること' do
expect {
post :create, params: valid_params
}.to change(Article, :count).by(1)
end
it '記事一覧にリダイレクトすること' do
post :create, params: valid_params
expect(response).to redirect_to(articles_path)
end
end
context '無効なパラメータの場合' do
let(:invalid_params) { { article: attributes_for(:article, title: '') } }
it '記事が作成されないこと' do
expect {
post :create, params: invalid_params
}.not_to change(Article, :count)
end
it '新規作成フォームを再表示すること' do
post :create, params: invalid_params
expect(response).to render_template(:new)
end
end
end
describe 'PUT #update' do
let(:article) { create(:article) }
context '有効なパラメータの場合' do
let(:new_title) { 'Updated Title' }
it '記事が更新されること' do
put :update, params: {
id: article.id,
article: { title: new_title }
}
expect(article.reload.title).to eq new_title
end
end
end
end
- JSONレスポンスのテスト
RSpec.describe Api::V1::ArticlesController, type: :controller do
describe 'GET #index' do
before do
create_list(:article, 3)
get :index, format: :json
end
it '正常なレスポンスを返すこと' do
expect(response).to have_http_status(:success)
end
it 'JSONフォーマットで記事一覧を返すこと' do
json = JSON.parse(response.body)
expect(json.size).to eq(3)
expect(json.first.keys).to include('title', 'content')
end
end
end
認証・認可のテストパターン
- Deviseを使用した認証テスト
RSpec.describe AdminController, type: :controller do
describe 'アクセス制御' do
context '未ログインの場合' do
it 'ログインページにリダイレクトすること' do
get :dashboard
expect(response).to redirect_to(new_user_session_path)
end
end
context '一般ユーザーでログインしている場合' do
let(:user) { create(:user) }
before { sign_in user }
it 'アクセスが拒否されること' do
get :dashboard
expect(response).to have_http_status(:forbidden)
end
end
context '管理者でログインしている場合' do
let(:admin) { create(:user, :admin) }
before { sign_in admin }
it 'ダッシュボードにアクセスできること' do
get :dashboard
expect(response).to be_successful
end
end
end
end
- Punditを使用した認可テスト
RSpec.describe ArticlesController, type: :controller do
describe '認可制御' do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
let(:other_user) { create(:user) }
context '記事の所有者の場合' do
before { sign_in user }
it '記事を更新できること' do
put :update, params: {
id: article.id,
article: { title: 'New Title' }
}
expect(response).to redirect_to(article_path(article))
end
it '記事を削除できること' do
expect {
delete :destroy, params: { id: article.id }
}.to change(Article, :count).by(-1)
end
end
context '記事の所有者でない場合' do
before { sign_in other_user }
it '記事の更新が拒否されること' do
put :update, params: {
id: article.id,
article: { title: 'New Title' }
}
expect(response).to have_http_status(:forbidden)
end
end
end
end
コントローラスペック作成時の重要なポイント:
- テストの構造化
- ログイン状態や権限に応じたコンテキストの分割
- 正常系と異常系のケースを漏れなくカバー
- 適切なセットアップとティアダウン
- セキュリティの考慮
- 認証・認可のバイパスができないことの確認
- セッション管理の適切性の検証
- CSRFトークンの検証
- パフォーマンスの考慮
- 必要最小限のデータセットアップ
- データベースクリーニングの適切な設定
- トランザクションの適切な使用
システムスペックによる統合テスト
Capybaraを使用したブラウザテストの基本
システムスペックは、実際のブラウザ操作をシミュレートして行う統合テストです。Capybaraを使用することで、ユーザーの行動を忠実に再現できます。
- 基本的なページ操作
RSpec.describe 'User管理', type: :system do
before do
driven_by(:rack_test)
end
describe 'ユーザー登録' do
it '新規ユーザーを登録できること' do
visit new_user_registration_path
fill_in 'ユーザー名', with: 'test_user'
fill_in 'メールアドレス', with: 'test@example.com'
fill_in 'パスワード', with: 'password123'
fill_in 'パスワード(確認)', with: 'password123'
click_button '登録'
expect(page).to have_content('アカウント登録が完了しました')
expect(current_path).to eq root_path
end
end
describe 'ログイン・ログアウト' do
let!(:user) { create(:user) }
it 'ログインとログアウトができること' do
visit new_user_session_path
fill_in 'メールアドレス', with: user.email
fill_in 'パスワード', with: user.password
click_button 'ログイン'
expect(page).to have_content('ログインしました')
click_link 'ログアウト'
expect(page).to have_content('ログアウトしました')
end
end
end
- よく使用するCapybaraのメソッド
| メソッド | 用途 | 例 |
|---|---|---|
| visit | ページ訪問 | visit root_path |
| click_on | リンク/ボタンクリック | click_on '送信' |
| fill_in | フォーム入力 | fill_in '名前', with: 'テスト' |
| select | セレクトボックス選択 | select '東京', from: '地域' |
| check | チェックボックス選択 | check '利用規約に同意' |
| expect(page) | ページ内容の確認 | expect(page).to have_content('成功') |
JavaScript動作のテスト方法
JavaScript動作を含むテストでは、実際のブラウザエンジンを使用する必要があります。
- JavaScriptテストの設定
RSpec.describe '動的フォーム', type: :system do
before do
driven_by(:selenium_chrome_headless)
end
describe '住所フォーム' do
it '郵便番号から住所が自動入力されること', js: true do
visit new_address_path
fill_in '郵便番号', with: '1500043'
# Ajax完了待機
wait_for_ajax
expect(find_field('都道府県').value).to eq '東京都'
expect(find_field('市区町村').value).to eq '渋谷区'
end
end
# カスタムヘルパーの定義
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
- モーダルウィンドウのテスト
RSpec.describe '商品管理', type: :system do
let!(:product) { create(:product) }
it '商品削除の確認モーダルが正しく動作すること', js: true do
visit products_path
click_on '削除'
within('.modal') do
expect(page).to have_content('本当に削除しますか?')
click_on '削除する'
end
expect(page).to have_content('商品を削除しました')
expect(page).not_to have_content(product.name)
end
end
よくあるユーザーテスト例
- 複雑なフォーム操作
RSpec.describe '記事管理', type: :system do
let(:user) { create(:user) }
before do
sign_in user
end
describe '記事作成' do
it 'タグと画像を含む記事を作成できること', js: true do
visit new_article_path
fill_in 'タイトル', with: 'テスト記事'
fill_in '本文', with: '記事の内容です'
# タグの追加
click_on 'タグを追加'
within('.tag-form:last-child') do
fill_in 'タグ名', with: 'Ruby'
end
# 画像のアップロード
attach_file '画像', Rails.root.join('spec/fixtures/test_image.jpg')
# プレビュー確認
click_on 'プレビュー'
within('.preview-modal') do
expect(page).to have_content('テスト記事')
expect(page).to have_css('img[alt="テスト記事"]')
click_on '閉じる'
end
click_on '投稿する'
expect(page).to have_content('記事を作成しました')
expect(page).to have_content('Ruby')
expect(page).to have_css('img[alt="テスト記事"]')
end
end
end
- ユーザーインタラクションのテスト
RSpec.describe 'ショッピングカート', type: :system do
let(:user) { create(:user) }
let!(:product) { create(:product) }
it '商品の購入フローが正常に完了すること', js: true do
sign_in user
visit product_path(product)
# カートに追加
select '2', from: '数量'
click_on 'カートに追加'
expect(page).to have_content('カートに追加しました')
# カート確認
click_on 'カートを見る'
expect(page).to have_content(product.name)
expect(page).to have_content('2個')
# お届け先入力
click_on 'レジに進む'
fill_in '氏名', with: '山田太郎'
fill_in '電話番号', with: '0300000000'
fill_in '郵便番号', with: '1500043'
wait_for_ajax
# 支払い方法選択
choose 'クレジットカード'
within('.card-form') do
fill_in 'カード番号', with: '4242424242424242'
fill_in '有効期限', with: '12/25'
fill_in 'セキュリティコード', with: '123'
end
click_on '注文を確定する'
expect(page).to have_content('ご注文ありがとうございました')
end
end
システムスペック作成時の重要なポイント:
- テストの安定性向上
- 適切な待機時間の設定
- Ajax通信の完了確認
- データベースのクリーンアップ
- パフォーマンス最適化
- headlessブラウザの使用
- 必要な場合のみJavaScriptドライバーを使用
- 共通のセットアップの活用
- 実践的なシナリオのカバー
- 実際のユーザー行動を模倣
- エッジケースの考慮
- 複数の機能を組み合わせたフロー
テストの保守性を高めるベストプラクティス
DRYなテストコードの書き方
テストコードの重複を減らし、保守性を高めるための手法を解説します。
- カスタムマッチャーの作成
# spec/support/matchers/be_recent_article.rb
RSpec::Matchers.define :be_recent_article do
match do |article|
article.published_at >= 1.week.ago &&
article.published_at <= Time.current
end
failure_message do |article|
"expected #{article.title} to be published within last week"
end
end
# 使用例
RSpec.describe Article do
let(:article) { create(:article) }
it { is_expected.to be_recent_article }
end
- コンテキスト共有のためのヘルパーモジュール
# spec/support/shared_contexts/admin_user_context.rb
RSpec.shared_context 'admin user context' do
let(:admin) { create(:user, :admin) }
before do
sign_in admin
allow(controller).to receive(:current_user).and_return(admin)
end
end
# 使用例
RSpec.describe AdminController do
include_context 'admin user context'
it '管理画面にアクセスできること' do
get :dashboard
expect(response).to be_successful
end
end
shared_examplesとshared_contextsの活用法
- shared_examplesの基本的な使い方
# spec/support/shared_examples/publishable.rb
RSpec.shared_examples 'publishable' do
let(:model) { described_class }
it { is_expected.to have_db_column(:published_at).of_type(:datetime) }
it { is_expected.to have_db_column(:status).of_type(:string) }
describe '#publish' do
let(:instance) { create(model.to_s.underscore.to_sym) }
it '公開状態に変更できること' do
instance.publish
expect(instance).to be_published
expect(instance.published_at).to be_present
end
end
end
# 使用例
RSpec.describe Article do
it_behaves_like 'publishable'
end
RSpec.describe Blog do
it_behaves_like 'publishable'
end
- パラメータ化されたshared_examples
RSpec.shared_examples 'status transition' do |from_status, to_status, method_name|
describe "##{method_name}" do
let(:instance) { create(described_class.to_s.underscore.to_sym, status: from_status) }
it "#{from_status}から#{to_status}に変更できること" do
expect { instance.send(method_name) }
.to change { instance.status }.from(from_status).to(to_status)
end
end
end
# 使用例
RSpec.describe Order do
it_behaves_like 'status transition', 'pending', 'confirmed', :confirm
it_behaves_like 'status transition', 'confirmed', 'shipped', :ship
end
テストデータ管理のベストプラクティス
- 効率的なファクトリ設計
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { 'password123' }
name { Faker::Name.name }
# 基本的な属性のみを定義
trait :basic do
# 最小限の属性のみを設定
end
# 必要に応じて追加の属性を定義
trait :with_profile do
after(:create) do |user|
create(:profile, user: user)
end
end
# 関連付けを含むケース
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, user: user)
end
end
# 特定の状態をまとめて定義
trait :admin_with_full_access do
admin { true }
after(:create) do |user|
create(:profile, :full_access, user: user)
create_list(:permission, 3, user: user)
end
end
end
end
- テストデータのクリーンアップ戦略
# spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
# 大きなデータセットを使用するテスト用
config.around(:each, :large_data) do |example|
DatabaseCleaner.strategy = :truncation
example.run
DatabaseCleaner.strategy = :transaction
end
end
- カスタムヘルパーメソッド
# spec/support/test_helpers.rb
module TestHelpers
def create_user_with_posts(post_count: 3)
user = create(:user)
create_list(:post, post_count, user: user)
user
end
def create_complete_order(product_count: 2)
order = create(:order)
create_list(:order_item, product_count, order: order)
order.calculate_total
order
end
end
RSpec.configure do |config|
config.include TestHelpers
end
保守性を高めるための重要なポイント:
- 命名規則の統一
- ファクトリの命名は一貫性を持たせる
- shared_examples/contextsは目的を明確に示す名前をつける
- カスタムマッチャーは動詞形式で命名
- モジュール化の推進
- 共通の振る舞いはshared_examplesに抽出
- 共通のセットアップはshared_contextsに配置
- ヘルパーメソッドは適切にモジュール化
- 設定の一元管理
- spec_helper.rbでの共通設定
- 環境変数の適切な管理
- タグやフィルターの統一的な設定
パフォーマンスとデバッグ
テストの実行速度を改善する方法
RSpecのテスト実行速度を最適化することで、開発サイクルを効率化できます。
- データベース最適化
# spec/support/database_performance.rb
RSpec.configure do |config|
config.before(:suite) do
# テストデータベースのインデックスを確認
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.indexes(table).each do |index|
puts "Table #{table} has index on #{index.columns.join(', ')}"
end
end
end
# トランザクションを効率的に使用
config.use_transactional_fixtures = true
# 大規模なデータセットのテスト用の設定
config.around(:each, :bulk_data) do |example|
ActiveRecord::Base.transaction do
example.run
raise ActiveRecord::Rollback
end
end
end
- テストの並列実行設定
# .rspec_parallel
--format progress
--format ParallelTests::RSpec::RuntimeLogger
--out tmp/parallel_runtime_rspec.log
# config/database.yml
test:
database: myapp_test
prepared_statements: false
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
reaping_frequency: <%= ENV.fetch("DB_REAP_FREQ") { 10 } %>
# 実行コマンド
bundle exec parallel_rspec spec/
- メモリ使用量の最適化
# spec/spec_helper.rb
RSpec.configure do |config|
config.before(:suite) do
# メモリリークを検出
require 'memory_profiler'
MemoryProfiler.start
end
config.after(:suite) do
report = MemoryProfiler.stop
report.pretty_print(scale_bytes: true)
end
# 大きなオブジェクトの解放
config.after(:each) do
GC.start
end
end
効率的なデバッグテクニック
- pry-byebugを使用したデバッグ
# Gemfile
group :development, :test do
gem 'pry-byebug'
end
# テスト内でのデバッグ例
RSpec.describe User do
it 'complex user operation' do
user = create(:user)
# デバッグポイントの設定
binding.pry
result = user.perform_complex_operation
expect(result).to be_success
end
end
# .pryrc
if defined?(PryByebug)
Pry.commands.alias_command 'c', 'continue'
Pry.commands.alias_command 's', 'step'
Pry.commands.alias_command 'n', 'next'
Pry.commands.alias_command 'f', 'finish'
end
- テスト実行の詳細ログ出力
# spec/support/debug_helpers.rb
module DebugHelpers
def debug_test_execution
puts "テスト開始: #{example.metadata[:full_description]}"
puts "パラメータ: #{example.metadata[:params]}" if example.metadata[:params]
yield
puts "テスト終了: #{example.metadata[:full_description]}"
rescue => e
puts "エラー発生: #{e.message}"
puts e.backtrace
raise e
end
end
RSpec.configure do |config|
config.include DebugHelpers
config.around(:each, :debug) do |example|
debug_test_execution { example.run }
end
end
CIパイプラインでのRSpec実行のコツ
- GitHub Actionsの設定例
# .github/workflows/rspec.yml
name: RSpec Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.0
bundler-cache: true
- name: Cache gems
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: Install dependencies
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Setup Database
run: |
cp config/database.yml.ci config/database.yml
bundle exec rails db:create
bundle exec rails db:schema:load
env:
RAILS_ENV: test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
- name: Run RSpec
run: bundle exec rspec
env:
RAILS_ENV: test
COVERAGE: true
- テスト結果の保存と分析
# spec/support/ci_helpers.rb
RSpec.configure do |config|
if ENV['CI']
# JUnit形式のレポート出力
config.add_formatter 'RSpec::JUnit::Formatter', 'results/rspec.xml'
# テストカバレッジの計測
require 'simplecov'
SimpleCov.start 'rails' do
add_filter '/spec/'
add_filter '/config/'
minimum_coverage 90
maximum_coverage_drop 5
end
end
end
- パフォーマンス最適化のためのベストプラクティス
- テストの分割実行
# lib/tasks/parallel_specs.rake
namespace :spec do
task :parallel do
# テストの分割実行
system "bundle exec parallel_rspec -n #{ENV['CI_NODE_TOTAL']} --only-group #{ENV['CI_NODE_INDEX']} spec/"
end
end
- テストの優先順位付け
# spec/spec_helper.rb
RSpec.configure do |config|
# 重要なテストを先に実行
config.register_ordering(:global) do |items|
items.sort_by do |item|
case item.metadata[:type]
when :model then 1
when :controller then 2
when :system then 3
else 4
end
end
end
end
パフォーマンスとデバッグに関する重要なポイント:
- 実行速度の最適化
- 不必要なデータベース操作の削減
- テストの並列実行の活用
- メモリ使用量の監視と最適化
- 効果的なデバッグ
- 適切なデバッグツールの選択
- ログ出力の戦略的な活用
- テスト環境の整備
- CI/CD統合のベストプラクティス
- キャッシュの効果的な活用
- テスト結果の可視化
- 実行環境の最適化