0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RSpecの基礎を完全に理解した

Posted at

はじめに

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()
  1. 実践例:完全なテストコード

ここまでの知識を使った実践的なテストコードです。

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. よくある間違いと対策

間違い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
  1. テストファイルの配置

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学習ロードマップ

  1. ✅ 基本構造を理解(describe, it, expect)
  2. ✅ マッチャーを覚える(eq, be_valid, match)
  3. ✅ FactoryBotを使う(build, create)
  4. ✅ let/let!/beforeを使い分ける
  5. ✅ モック/スタブを理解する

覚えておくべき重要ポイント

項目 判断基準
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

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?