【Ruby on Rails】RSpecでモデルのテストを書く

Ruby

こんにちは、かつコーチです。

「テストは大事」と分かっていても、何をどこまで書けばいいのか迷った経験はありませんか。

この記事では、RSpecの導入からモデルのバリデーション・アソシエーションのテストの書き方までを整理します。

FactoryBotを使ったテストデータの準備方法にも触れるので、実務レベルのモデルテストを書けるようになりましょう。

rspec-railsの導入手順

Gemfileへの追加

rspec-railsは開発・テスト環境でのみ必要なので、:development, :testグループに追加します。

# Gemfile
group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
end

追加したらインストールし、RSpecの初期設定を生成します。

bundle install
rails generate rspec:install

このコマンドで、以下のファイルが生成されます。

  • .rspec(RSpecの実行オプション設定)
  • spec/spec_helper.rb(RSpec本体の設定)
  • spec/rails_helper.rb(Rails関連の設定を読み込む)

rails_helperの基本設定

spec/rails_helper.rbには、テストDBの自動マイグレーションチェックなどが最初から記述されています。

FactoryBotを使う場合は、以下の1行を追加しておくと便利です。

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

この設定により、テストコード内でFactoryBot.create(:user)ではなくcreate(:user)と短く書けるようになります。

モデルのバリデーション・アソシエーションのテスト例

バリデーションのテスト

まずは対象となるモデルを用意します。

# app/models/article.rb
class Article < ApplicationRecord
  belongs_to :user
  validates :title, presence: true, length: { maximum: 100 }
  validates :body, presence: true
end

このモデルに対するテストは、spec/models/article_spec.rbに書きます。

# spec/models/article_spec.rb
require "rails_helper"

RSpec.describe Article, type: :model do
  describe "バリデーション" do
    it "titleが空だと無効であること" do
      article = build(:article, title: nil)
      expect(article).not_to be_valid
      expect(article.errors[:title]).to include("を入力してください")
    end

    it "titleが100文字を超えると無効であること" do
      article = build(:article, title: "あ" * 101)
      expect(article).not_to be_valid
    end

    it "titleとbodyが揃っていれば有効であること" do
      article = build(:article)
      expect(article).to be_valid
    end
  end
end

buildはDBへの保存を行わず、メモリ上にインスタンスを生成します。

保存を伴わないバリデーションのテストでは、createより高速なbuildを使うのが基本です。

アソシエーションのテスト

アソシエーションが正しく機能しているかは、関連レコードの取得結果で確認します。

# spec/models/article_spec.rb
RSpec.describe Article, type: :model do
  describe "アソシエーション" do
    it "userに紐付いていること" do
      user = create(:user)
      article = create(:article, user: user)
      expect(article.user).to eq(user)
    end
  end
end

shoulda-matchersgemを導入すると、アソシエーションの定義自体をより簡潔にテストできます。

# Gemfile
group :test do
  gem "shoulda-matchers"
end
# spec/models/article_spec.rb
RSpec.describe Article, type: :model do
  it { should belong_to(:user) }
  it { should validate_presence_of(:title) }
end

コード量が減る一方、内部で何を検証しているか分かりにくくなる面もあります。

チームの習熟度に応じて使い分けるとよいでしょう。

FactoryBotでテストデータを準備する

buildcreateを使うには、事前にFactoryを定義しておく必要があります。

# spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    title { "テスト記事のタイトル" }
    body { "テスト記事の本文です。" }
    association :user
  end
end
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "user#{n}@example.com" }
    password { "password123" }
  end
end

sequenceを使うことで、一意制約のあるemailカラムでも重複エラーを避けられます。

FactoryBotの詳しい使い方は別記事で扱いますが、まずはこの最小構成を覚えておけば十分です。

よくあるつまずきポイント・エラー対処

一意制約テストでのハマりどころ

❌Before

# spec/models/user_spec.rb
it "emailが重複していると無効であること" do
  create(:user, email: "test@example.com")
  user = build(:user, email: "test@example.com")
  expect(user).not_to be_valid
end

このテストを実行したところ、意図通りにパスしました。

しかし別の開発者が同じテストファイルに追記したところ、次のエラーが出るようになりました。

ActiveRecord::RecordInvalid: Validation failed: Email has already been taken

原因は、Factoryのsequenceを使わず固定のemailを直書きしていたため、他のテストと値が衝突していたことでした。

✅After

# spec/models/user_spec.rb
it "emailが重複していると無効であること" do
  existing_user = create(:user)
  user = build(:user, email: existing_user.email)
  expect(user).not_to be_valid
end

既存レコードのemailを再利用する形にすることで、Factory側のsequence定義とテストコードの整合性を保てました。

一意制約を検証するテストでは、固定値の直書きを避けるのが安全です。

まとめ

この記事のポイント

  • rspec-railsgem "rspec-rails"追加後、rails generate rspec:installで導入する
  • バリデーションのテストは、DB保存を伴わないbuildを基本にする
  • アソシエーションのテストは、実際のレコード生成かshoulda-matchersで検証する
  • FactoryBotのsequenceは、一意制約のあるカラムのテストで重複を避けるために使う
  • 固定値の直書きは、他のテストとの衝突原因になりやすい

次に読むべき記事

  • System SpecでE2Eテストを書く
  • バリデーションの基本

タグ: Ruby on Rails, 中級者向け, テスト

タイトルとURLをコピーしました