はじめに
Railsプロジェクトにアサインされたものの、RSpecの書き方や読み方がわからず困っていませんか?
この記事では、RSpec初学者が最初につまずきやすいポイントを実践的なコード例とともに解説します。
この記事で学べること
- RSpecとRailsの親和性
-
describe/context/itの使い分け -
let/let!/beforeの違いと使い分け - マッチャーの種類と使い方
- FactoryBotによるテストデータ作成
- モックとスタブの基礎
対象読者
- RSpecを初めて学ぶ方
- Railsのテストコードを読めるようになりたい方
- テストコードレビューができるようになりたいQAエンジニア
- MinitestからRSpecへの移行を検討している方
1. RSpecとは?なぜRailsと相性が良いのか
RSpecの特徴
RSpec(Ruby Spec)は、RubyのためのBDD(振る舞い駆動開発)テストフレームワークです。
Railsとの相性が良い理由
| 特徴 | 説明 |
|---|---|
| 読みやすい自然言語スタイル |
describe, it, expect などで人間が読める形式 |
| Rails専用の拡張 |
rspec-rails gemがRailsの機能に対応 |
| 豊富なマッチャー | バリデーション、アソシエーションなどの検証が簡単 |
| エコシステム | FactoryBot、Shoulda Matchersなど優れたツールが充実 |
2. RSpecの基本構造:describe / context / it の使い分け
基本的な考え方
RSpecのテストは、以下の3つの要素で構造化されます:
RSpec.describe User, type: :model do # ← テストの対象
describe 'validations' do # ← どの機能をテストするか
context 'when name is present' do # ← どんな状況か
it 'is valid' do # ← 期待される結果
user = build(:user, name: 'テストユーザー')
expect(user).to be_valid
end
end
end
end
使い分けのルール
| キーワード | 役割 | 例 |
|---|---|---|
| describe | 何をテストするか | describe User, describe '#save' |
| context | どんな時にテストするか | context 'when user is admin' |
| it | どうあるべきか | it 'returns true' |
実践例:ユーザー登録のテスト
RSpec.describe User, type: :model do
describe 'validations' do
describe 'email' do
context 'when email is valid' do
it 'is valid' do
user = build(:user, email: 'test@example.com')
expect(user).to be_valid
end
end
context 'when email is invalid' do
it 'is invalid' do
user = build(:user, email: 'invalid')
expect(user).not_to be_valid
end
it 'has error message' do
user = build(:user, email: 'invalid')
user.valid?
expect(user.errors[:email]).to include('is invalid')
end
end
end
end
end
自然言語として読むと:
Userモデルの
バリデーションについて、
email属性は、
メールアドレスが有効な場合、
有効である
これがBDD(振る舞い駆動開発)の本質です!
3. let / let! / before の違いと使い分け
これは初学者が最も混乱するポイントです。
3.1 let とは?
let は遅延評価される変数定義です。使われるまで実行されません。
describe 'User' do
let(:user) { create(:user) } # ← ここでは実行されない(定義だけ)
it 'has a name' do
# ↓ ここで初めて create(:user) が実行される
expect(user.name).to be_present
end
it 'counts users' do
expect(User.count).to eq(0) # ← user を呼んでいないので 0件
end
end
3.2 let! とは?
let! は即座に評価されます。itブロックの前に必ず実行されます。
describe 'User' do
let!(:user) { create(:user) } # ← itブロックの前に実行される
it 'counts users' do
expect(User.count).to eq(1) # ← 既に1件存在する
end
end
3.3 before とは?
before は各テストの前に実行されるセットアップ処理です。
describe 'User' do
before do
@user = create(:user) # ← インスタンス変数に保存
ActionMailer::Base.deliveries.clear # ← メールリストをクリア
end
it 'has a user' do
expect(@user).to be_present # ← @user で参照
end
end
3.4 使い分けの判断基準
シンプルな判断フロー
「後で変数として使いたいか?」
- YES → let! を使う
- NO → before を使う
詳細な使い分け表
| 用途 | 使うもの | 例 |
|---|---|---|
| テストデータを作成して後で参照 | let! | let!(:user) { create(:user) } |
| 環境設定・クリーンアップ | before | before { ActionMailer::Base.deliveries.clear } |
| モック・スタブの設定 | before | before { allow(API).to receive(:call) } |
| メモリ効率を重視したい | let | let(:user) { create(:user) } |
実践例:重複メールアドレスのテスト
describe 'email uniqueness' do
# 既存ユーザーとして参照したい → let!
let!(:existing_user) { create(:user, email: 'taken@example.com') }
# メールリストのクリーンアップ → before
before do
ActionMailer::Base.deliveries.clear
end
it 'cannot register duplicate email' do
new_user = build(:user, email: 'taken@example.com')
expect(new_user).not_to be_valid
# existing_user を直接参照できる
expect(new_user.email).to eq(existing_user.email)
end
end
4. FactoryBotによるテストデータ作成
4.1 FactoryBotとは?
テスト用のダミーデータを簡単に作成するツールです。
Factory定義
spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:name) { |n| "テストユーザー#{n}" } # ユニークな値を自動生成
sequence(:email) { |n| "test#{n}@example.com" }
password { 'Password123' }
password_confirmation { 'Password123' }
# Trait(特性):特定の状態を定義
trait :admin do
role { 'admin' }
end
trait :with_profile_image do
after(:create) do |user|
user.profile_image.attach(
io: File.open('spec/fixtures/files/sample.jpg'),
filename: 'sample.jpg'
)
end
end
end
end
4.2 基本的な使い方
インスタンスを作成(DBに保存しない)
user = build(:user)
インスタンスを作成してDBに保存
user = create(:user)
ハッシュを生成
attrs = attributes_for(:user)
Traitを使用
admin = create(:user, :admin)
user_with_image = create(:user, :with_profile_image)
# 複数のTraitを組み合わせ
user = create(:user, :admin, :with_profile_image)
# 属性を上書き
user = create(:user, name: '特定の名前', email: 'specific@example.com')
# 複数作成
users = create_list(:user, 3) # 3人のユーザーを作成
4.3 build と create の使い分け
| メソッド | DBに保存? | 用途 |
|---|---|---|
| build | ❌ しない | バリデーションテスト、新規作成のテスト |
| create | ✅ する | 既存データが必要なテスト、アソシエーションのテスト |
バリデーションテスト → build
it 'is invalid without name' do
user = build(:user, name: nil)
expect(user).not_to be_valid
end
# 既存データが必要 → create
it 'cannot have duplicate email' do
create(:user, email: 'taken@example.com') # 既存データを作成
user = build(:user, email: 'taken@example.com')
expect(user).not_to be_valid
end
5. マッチャー完全ガイド
マッチャーは、期待値と実際の値を照合するツールです。
5.1 基本マッチャー
等価性
eq(等しい)
expect(user.name).to eq('太郎')
expect(user.age).not_to eq(0)
真偽値(be_xxx系)
be_valid(バリデーションが通る)
expect(user).to be_valid
expect(user).not_to be_valid
be_present(値が存在する)
expect(user.email).to be_present
be_persisted(DBに保存されている)
expect(user).to be_persisted
be_attached(ファイルが添付されている)
expect(user.profile_image).to be_attached
仕組み:
expect(user).to be_valid
↓ 実際には
expect(user.valid?).to eq(true)
範囲
be_between(範囲内)
expect(user.age).to be_between(18, 100)
expect(id.length).to be_between(7, 18)
パターンマッチ(正規表現)
match
expect(user.email).to match(/@/)
expect(user.id).to match(/[a-z]/) # 小文字を含む
expect(user.id).to match(/[A-Z]/) # 大文字を含む
expect(user.id).to match(/\d/) # 数字を含む
コレクション
include(含む)
expect([1, 2, 3]).to include(2)
expect(user.errors[:name]).to include('を入力してください')
5.2 変更検証マッチャー(重要!)
change(値の変化を検証)
expect { create(:user) }.to change(User, :count).by(1)
expect { user.destroy }.to change(User, :count).by(-1)
from().to()
expect { user.update(name: '新しい名前') }
.to change { user.name }.from('古い名前').to('新しい名前')
変化しないことを検証
expect { some_action }.not_to change(User, :count)
実行フロー:
実行前の状態
User.count # => 0
expect do
create(:user) # ← ここでUserが1件作成される
end.to change(User, :count).by(1)
実行後の状態
User.count # => 1
5.3 Rails専用マッチャー(Shoulda Matchers)
バリデーション
# validate_presence_of(必須)
it { should validate_presence_of(:name) }
# validate_length_of(文字数)
it { should validate_length_of(:name).is_at_most(10) }
# validate_uniqueness_of(一意性)
it { should validate_uniqueness_of(:email).case_insensitive }
手動で書くと:
# Shoulda Matchersなし(冗長)
it 'requires name' do
user = build(:user, name: nil)
expect(user).not_to be_valid
expect(user.errors[:name]).to include('を入力してください')
end
Shoulda Matchersあり(1行で済む!)
it { should validate_presence_of(:name) }
アソシエーション
has_many
it { should have_many(:posts).dependent(:destroy) }
# belongs_to
it { should belong_to(:user) }
# has_one_attached(Active Storage)
it { should have_one_attached(:avatar) }
5.4 マッチャー選択フローチャート
何を検証したい?
├─ 値の等価性
│ └─ eq, be
│
├─ 真偽値(〜か?)
│ └─ be_valid, be_present, be_persisted
│
├─ 範囲
│ └─ be_between
│
├─ パターン
│ └─ match(正規表現)
│
├─ 配列に含まれる
│ └─ include
│
├─ 件数の変化
│ └─ change().by()
│
└─ Rails のバリデーション/アソシエーション
└─ validate_presence_of, have_many
6. モックとスタブの基礎
6.1 スタブ(Stub)
既存のメソッドの戻り値を偽装すること。
基本形
allow(オブジェクト).to receive(:メソッド名).and_return(偽の戻り値)
実例:外部API呼び出しを偽装
allow(WeatherAPI).to receive(:get_temperature).and_return(25)
実例:時間のかかる処理をスキップ
allow(user).to receive(:send_welcome_email).and_return(true)
いつ使う?
- 外部API呼び出しを避けたい
- メール送信をスキップしたい
- 時間のかかる処理を回避したい
6.2 モック(Mock)
メソッドが呼ばれたことを検証すること。
基本形
expect(オブジェクト).to receive(:メソッド名)
実例:メソッドが呼ばれることを検証
it 'sends welcome email' do
user = build(:user)
expect(user).to receive(:send_welcome_email)
user.save
end
6.3 まとめ
| 概念 | 目的 | 例 |
|---|---|---|
| スタブ | メソッドの戻り値を偽装 | allow().to receive().and_return() |
| モック | メソッドが呼ばれたか検証 | expect().to receive() |
- 実践例:完全なテストコード
ここまでの知識を使った実践的なテストコードです。
spec/models/user_spec.rb
RSpec.describe User, type: :model do
# ========== バリデーションテスト ==========
describe 'validations' do
describe 'name' do
# Shoulda Matchers で簡潔に
it { should validate_presence_of(:name) }
it { should validate_length_of(:name).is_at_most(10) }
# 個別のケースを詳しく検証
it 'is valid with 10 characters' do
user = build(:user, name: 'あ' * 10)
expect(user).to be_valid
end
it 'is invalid with 11 characters' do
user = build(:user, name: 'あ' * 11)
expect(user).not_to be_valid
expect(user.errors[:name]).to include('は10文字以内で入力してください')
end
end
describe 'email' do
subject { build(:user) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
context 'when email format is invalid' do
it 'is invalid without @' do
user = build(:user, email: 'invalid')
expect(user).not_to be_valid
end
end
end
end
# ========== アソシエーションテスト ==========
describe 'associations' do
it { should have_many(:posts).dependent(:destroy) }
it { should have_one_attached(:avatar) }
end
# ========== コールバックテスト ==========
describe 'callbacks' do
describe 'before_create' do
it 'generates unique_id automatically' do
user = create(:user)
expect(user.unique_id).to be_present
expect(user.unique_id.length).to be_between(7, 18)
end
it 'contains lowercase, uppercase, and digits' do
user = create(:user)
expect(user.unique_id).to match(/[a-z]/)
expect(user.unique_id).to match(/[A-Z]/)
expect(user.unique_id).to match(/\d/)
end
end
end
end
spec/requests/users_spec.rb
RSpec.describe 'Users', type: :request do
describe 'POST /users' do
# let!: パラメータを変数として使う
let!(:valid_params) do
{
user: {
name: 'テストユーザー',
email: 'test@example.com',
password: 'Password123',
password_confirmation: 'Password123'
}
}
end
# before: 環境設定
before do
ActionMailer::Base.deliveries.clear
end
context 'with valid parameters' do
it 'creates a new user' do
expect { post '/users', params: valid_params }
.to change(User, :count).by(1)
end
it 'redirects to user page' do
post '/users', params: valid_params
expect(response).to have_http_status(:redirect)
end
end
context 'with invalid parameters' do
it 'does not create a user' do
invalid_params = valid_params.deep_dup
invalid_params[:user][:email] = 'invalid'
expect { post '/users', params: invalid_params }
.not_to change(User, :count)
end
end
end
end
- よくある間違いと対策
間違い1: expectの使い方
❌ 間違い
expect(user.valid?).to eq(true)
✅ 正しい(be_valid を使う)
expect(user).to be_valid
間違い2: changeの使い方
❌ 間違い(.to が抜けている)
expect { create(:user) } change(User, :count).by(1)
✅ 正しい
expect { create(:user) }.to change(User, :count).by(1)
間違い3: beforeで変数を作ろうとする
❌ 間違い(ローカル変数なのでitブロックで使えない)
before do
user = create(:user)
end
it 'test' do
expect(user.name).to eq('test') # ← エラー!user is undefined
end
✅ 正しい(let!を使う)
let!(:user) { create(:user) }
it 'test' do
expect(user.name).to eq('test') # ← OK!
end
- テストファイルの配置
Railsの慣習に従ったファイル配置:
app/models/user.rb
→ spec/models/user_spec.rb
app/controllers/users_controller.rb
→ spec/requests/users_spec.rb
app/services/payment_service.rb
→ spec/services/payment_service_spec.rb
まとめ
RSpec学習ロードマップ
- ✅ 基本構造を理解(describe, it, expect)
- ✅ マッチャーを覚える(eq, be_valid, match)
- ✅ FactoryBotを使う(build, create)
- ✅ let/let!/beforeを使い分ける
- ✅ モック/スタブを理解する
覚えておくべき重要ポイント
| 項目 | 判断基準 |
|---|---|
| describe vs context | describe = 「何を」、context = 「どんな時に」 |
| let vs let! | 後で変数として使う? YES → let! |
| let! vs before | 変数として使う? YES → let! / NO → before |
| build vs create | DBに保存する必要がある? YES → create |
| stub vs mock | 戻り値を偽装? YES → stub / 呼び出しを検証? YES → mock |
参考資料