9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】RSpecの導入からモデルテストを書き方まで|FactoryBot・Fakerでハマったポイントまとめ

9
Posted at

はじめに

個人開発しているRailsアプリ「MabaTalk」にRSpecを導入し、主要モデルであるMessageCategoryのテストを書きました。

テストを書いた目的は2つです。

  • リグレッション検知 :コード変更によって既存の動く機能が壊れたことを検知するため
  • 設計の言語化 :「なぜこの設計にしたか」をテストで表現するため

この記事では、RSpecの導入手順から実際のテストの書き方まで、つまずいたポイントとともにまとめます。

想定読者

  • RSpecを導入したが、何をテストすればいいかわからない方
  • Railsでモデルテストを書いてみたい方

環境

  • Ruby on Rails 7.2
  • Docker環境
  • rspec-rails 8.0.4
  • factory_bot_rails 6.5.1
  • faker 3.6.1

1. gemのインストール

Gemfiledevelopment, :test グループに追加します。

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "faker"
end
bundle install

各gemの役割

  • rspec-rails:RailsでRSpecを使うためのテストフレームワーク
  • factory_bot_rails :テスト用データ(Factory)を簡単に作成できる
  • faker :ダミーデータ(名前・メールアドレスなど)を生成する

development, :test に入れる理由
rails generate rspec:install のようなジェネレータはdevelopment環境で実行するためです。

今回のテストでは、MessageCategoryが「System共通」と「ユーザー固有」の両方のデータを扱うため、複数のユーザーやカテゴリのデータを用意する必要がありました。
そのため、FactoryBotでテストデータの構造を定義し、Fakerを使って名前やメールアドレスなどのダミーデータを生成しています。
Fakerを使うことで、バリデーションに引っかからない現実的な値を簡単に用意できるため、テストデータの作成を効率化できます。

2. RSpecの初期設定

以下のコマンドでRSpecの初期設定を行います。

rails generate rspec:install

このコマンドにより、RSpecを実行するために必要な設定ファイルが生成されます。

.rspec
spec/
spec/spec_helper.rb
spec/rails_helper.rb
  • spec_helper.rb:RSpecの基本設定 (Railsに依存しない軽量な設定)
  • rails_helper.rb:Rails環境を読み込んだ上でのテスト設定(モデル・DBなどを扱う場合はこちらを使用)
    モデルテストなどRailsの機能を使う場合は、rails_helperを読み込む必要があります。

3. FactoryBotの設定

spec/rails_helper.rbRSpec.configure ブロックに追加します。

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  # ...
end

この設定を行うことで、テスト内で FactoryBot.create(:user)create(:user) のように省略して書けるようになります。

毎回FactoryBotと書く必要がなくなるため、テストコードの可読性が向上。

また、FactoryBotはテストデータを簡単に作成するためのライブラリであり、
モデルの関連やバリデーションを考慮したデータを効率よく用意することができます。

公式ドキュメント:factory_bot GitHub README

4. Factoryの作成

テストでは、毎回データを用意する必要があるため、FactoryBotを使ってテスト用のデータの定義を行います。

Factoryを定義しておくことで、毎回同じ構造のデータを簡単に生成でき、テストコードをシンプルに保つことができます。

MessageCategoryのFactory

# spec/factories/message_categories.rb
FactoryBot.define do
  factory :message_category do
    name { Faker::Lorem.word }
    kana { Faker::Lorem.word }
    icon { IconDefinitions::CATEGORY_ICONS.keys.first }
  end
end

ポイント
icon はバリデーションで CATEGORY_ICONS.keys に含まれる値のみ許可しているため、keys.first で有効な値を取得しています。

UserのFactory

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Lorem.word }
    email { Faker::Internet.email }
    password { Faker::Internet.password(min_length: 6) }
  end
end

ポイント
emailFaker::Lorem.word ではメール形式にならずバリデーションエラーになります。そのため、Faker::Internet.email を使うことで、バリデーションを通る値を生成できます。

5. テストを書く

モデルのコード

class MessageCategory < ApplicationRecord
  belongs_to :user, optional: true
  validates :name, :kana, :icon, presence: true
  validates :name, uniqueness: { scope: :user_id }
  validates :key, presence: true, uniqueness: true
  validates :icon, inclusion: { in: ->(_) { IconDefinitions::CATEGORY_ICONS.keys } }
  validates :icon_color, inclusion: { in: ->(_) { IconDefinitions::ICON_COLORS.keys } }, if: :user_id?

  before_validation :set_key, on: :create

  scope :for_user, ->(user) {
    if user
      where(user_id: [nil, user.id])
    else
      where(user_id: nil)
    end
  }

  private

  def set_key
    self.key ||= SecureRandom.uuid
  end
end

テストすべき項目

このモデルで確認すべき項目を整理しました。
テストは「すべてを書く」のではなく、壊れたときに影響が大きい部分を優先して書くことを意識しました。

バリデーション(不正データ防止)

  • name / kana / icon が必須
  • 同じuser_id内でnameが重複不可
  • iconはCATEGORY_ICONSに含まれる値のみ
  • icon_colorはuser_idがある場合のみICON_COLORSに含まれる値のみ

→ 不正なデータが保存されると、UIや機能全体に影響が出るため

スコープ(データの見え方)

  • for_user(user) → system共通 + そのユーザーのレコードを返す
  • for_user(nil) → system共通のみ返す

→ 他ユーザーのデータが取得されると、情報漏洩につながるため

バリデーションのテスト

describe 'バリデーションチェック' do
  it "nameがないと保存できない" do
    message_category = build(:message_category, name: nil)
    expect(message_category).to be_invalid
  end

  it "kanaがないと保存できない" do
    message_category = build(:message_category, kana: nil)
    expect(message_category).to be_invalid
  end

  it "iconがないと保存できない" do
    message_category = build(:message_category, icon: nil)
    expect(message_category).to be_invalid
  end
end

build を使う理由
バリデーションテストでは create ではなく build を使うことが一般的です。

build はDBに保存せず、オブジェクトの有効・無効のみを確認できるため、バリデーションのテストに適しています。

一方で create はDBに保存を行うため、テストの目的によって使い分ける必要があります。

スコープのテスト

このアプリでは user_id: nil がsystem共通レコードを意味します。for_user(user) はsystem共通レコードとそのユーザーのレコードを返す設計です。

describe 'スコープ' do
  it "for_user(user)はsystem共通とそのユーザーのレコードを返す" do
    user_a = create(:user)
    user_b = create(:user)
    message_category_a = create(:message_category, user_id: user_a.id)
    message_category_b = create(:message_category, user_id: user_b.id)
    system_category = create(:message_category, user_id: nil)

    result = MessageCategory.for_user(user_a)

    expect(result).to include(message_category_a)
    expect(result).to include(system_category)
    expect(result).not_to include(message_category_b)
  end
end

このテストでは「正しいデータが含まれること」だけでなく、「含まれてはいけないデータが含まれていないこと」も確認しています。

not_to include を書く理由
「返すべきでないものが返っていないか」の確認が必要です。他ユーザーのレコードが含まれないことを確認することで、情報漏洩を防ぐセキュリティ上の保証になります。

また、1ユーザーのみでテストすると、誤って全件取得してしまうバグに気づけない可能性があるため、あえて複数ユーザーでテストしています。

6. テスト実行

以下のコマンドでテストを実行します。

bundle exec rspec spec/models/message_category_spec.rb

テストがすべて成功すると、以下のように表示されます。

....

Finished in 0.37 seconds
4 examples, 0 failures

4 examples は実行されたテストの数、0 failures は失敗したテストがないことを意味します。

テストがすべて通ることで、今回実装したバリデーションやスコープが正しく動作していることを確認できます。

また、将来的にコードを変更した際に、意図しない変更が発生していないかを検知することもできます。

つまずいたポイント

① モデルが読み込まれないエラー

uninitialized constant MessageCategory

テスト実行時、上記エラーが発生しました。

最初はFactoryやRSpecの書き方が間違っているのでは?と疑いましたが、エラーメッセージをよく見ると、モデル自体が読み込まれていない状況でした。

調べたところ、require 'rails_helper' を書き忘れており、Railsの環境が読み込まれていないことが原因でした。

rails_helperを読み込んでいない場合、Railsが起動していない状態になるため、
モデル(MessageCategoryクラス)が読み込まれず、定数として認識されません。

② Factoryの値によるバリデーションエラー

メールアドレスは不正な値です

テスト実行時、上記エラーが発生しました。

原因は、UserのFactoryで email { Faker::Lorem.word } と書いていたことです。
この値はメールアドレスの形式になっていないため、バリデーションに引っかかっていました。

Faker::Internet.email に変えて解決。

Factoryの値は「とりあえず動けばいい」ではなく、
実際のバリデーションを満たすデータを設定する必要があると学びました。

まとめ

  • バリデーションテストはbuildを使い、DBに依存せずオブジェクトの有効・無効を確認する
  • スコープテストは「返すべきもの」と「返すべきでないもの」の両方を検証することで、意図しないデータ取得(情報漏洩など)を防ぐ
  • Factoryの値は単なるダミーではなく、モデルのバリデーションを満たす前提で設計する

今回の実装を通して、「テストは動作確認」ではなく、モデルの責務や設計意図を明確にするためのものだと理解できました。

理解度チェッククイズ

ここまでの内容をもとに、理解度を確認するクイズを用意しました。

Q1. buildcreate の違いは何ですか?

A. build はDBに保存されるが、create は保存されない
B. build はDBに保存されず、create は保存される
C. 両方ともDBに保存される
D. 両方ともDBに保存されない

答え

B

解説
build はメモリ上にオブジェクトを作るだけで、DBには保存されません。
一方 create はDBに保存されるため、副作用(レコード増加)が発生します。
そのため、バリデーションの確認だけをしたい場合は build が適しています。


Q2. be_invalid は何を確認していますか?

expect(message_category).to be_invalid

A. DBに保存されたこと
B. バリデーションが通ったこと
C. バリデーションが通らないこと
D. エラーが発生したこと

答え

C
解説
be_invalid は valid? が false であることを確認しています。
つまり「このオブジェクトはバリデーションを満たしていない」という状態をテストしています。


Q3. 次のコードは何を取得しますか?

where(user_id: nil)

A. すべてのレコードが返る
B. user_idがnilのレコードのみ返る
C. エラーになる
D. 空配列が返る

答え

B
解説
where(user_id: nil) は「user_idがNULLのレコード」を取得します。
ActiveRecordではnilはSQLのNULLとして扱われ、その条件に一致するレコードのみが返ります。


Q4. スコープのテストで「含まれないこと」を確認する理由はどれですか?

A. テストの行数を増やすため
B. コードを見やすくするため
C. 不要なデータが混ざっていないか確認するため
D. DBの速度を上げるため

答え

C
解説
「正しいデータが含まれること」だけでは不十分で、「含まれてはいけないデータ」が入っていないかも重要です。
特に他ユーザーのデータが混ざると、情報漏洩につながるため重要なテストです。


Q5. Factoryで Faker::Internet.email を使う理由はどれですか?

A. ランダムな数字を生成するため
B. メール形式の文字列を生成するため
C. DBに保存しやすくするため
D. テストを早くするため

答え

B

解説
モデルにメール形式のバリデーションがある場合、不正な形式だとテストが失敗してしまいます。
Faker::Internet.email を使うことで、バリデーションを通る値を生成できます。


Q6. before_validation :set_key, on: :create の動きとして正しいのはどれですか?

A. すべてのタイミングで実行される
B. create時のバリデーション前のみ実行される
C. update時のみ実行される
D. save時に毎回実行される

答え

B

解説
on: :create が指定されているため、新規作成時のみ実行されます。
update時には実行されないため、既存データには影響しません。

9
2
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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?