RSpec - FactoryBotを導入し、効率良く意味のあるデータを作成する
「Everyday Rails - RSpecによるRailsテスト入門 - 第4章 意味のあるテストデータの作成 」の復習用まとめ。
本章のゴール
Factory Botに焦点を当て、
- ファクトリの利点と欠点
- ファクトリを用いたテスト実装
- その他、ファクトリのリファクタリングや"関連"を表す高度な使い方
- ファクトリを使いすぎるリスク
ファクトリ対フィクスチャ
フィクスチャ
Railsのデフォルト機能。しかし、以下の理由で使わない。
- テストデータがファイルに分割されるため一覧化できず、実装中に覚えておく必要あり
- データをデータ ースに読み込む際にActive Recordを使わない
- そのためバリデーションなどが無視され、本番データと一致しない可能性あり
Factory Botのインストール
group :development, :test do gem 'rspec-rails', '~> 3.6.0' gem 'factory_bot_rails', '~> 4.10.0' end
ジェネレータでモデル作成時に、自動的にファクトリを作成するように設定
confing/application.rbにおいて、fixtures: false の行を削除する
config.generators do |g| g.test_framework :rspec, # fixtures: false, view_specs: false, helper_specs: false, routing_specs: false end
ファクトリの作成
ファイルの作成
bin/rails g factory_bot:model user
spec/factories/users.rbが作成される
データの作成
FactoryBot.define do factory :user do first_name "Aaron" last_name "Sumner" email "test@example.com" password "test-password" end end
スペックでFactoryを使う
ファクトリが適切に設定されているかテスト
it "has a valid factory" do expect(FactoryBot.build(:user)).to be_valid end
バリデーションのテスト
正常系
# 有効なファクトリを持つこと it "has a valid factory" do user = User.build(:user) expect(user).to be_valid end
異常系
# 名がなければ無効な状態であること it "is invalid without a first name" do # user = User.new(first_name: nil) user = FactoryBot.build(:user, first_name: nil) user.valid? expect(user.errors[:first_name]).to include("can't be blank") end
該当の項目のみオーバーライド(上書き)することで、ピンポイントで明示的にテストし、分かりやすい。
テスト対象をスペック側でオーバーライドして可読性を高める
# ユーザーのフルネームを文字列として返すこと it "returns a user's full name as a string" do user = FactoryBot.build(:user, first_name: "John", last_name: "Doe") expect(user.name).to eq "John Doe" end
first_nameとlast_nameをあえて指定してテストしている。これにより自己文書的なテストになり、分かりやすい。
シーケンスを使ってユニークなデータを作成する
問題のあるコード
spec/factories/user.rb
FactoryBot.define do factory :user do first_name "Aaron" last_name "Sumner" email "test@example.com" password "test-password" end end
spec/models/user_spec.rb
# 複数のユーザーで何かする it "does something with multiple users" do user1 = FactoryBot.create(:user) user2 = FactoryBot.create(:user) expect(true).to be_truthy end
理由
上記コードだと、emailが重複する。
そのためuser2作成時にバリデーションに引っかかる。
シーケンスを使って上記問題を解決
FactoryBot.define do factory :user do first_name "Aaron" last_name "Sumner" sequence(:email) { |n| "tester#{n}@example.com" } password "test-password" end end
ファクトリで関連を扱う
associationでファクトリを呼び出し、インスタンスを生成
spec/factories/notes.rb
FactoryBot.define do factory :note do message "My important note." association :project user { project.owner } end end
上記のように記述すると、
note = FactoryBot.create(:note)
を実行した場合に、project(やuser)のインスタンスも自動で生成される。
誤った書き方
FactoryBot.define do factory :note do message "My important note." association :project association :user # user { project.owner } end end
association :userと記述してしまうと、
と、重複してインスタンスを生成してしまう。
spec/factories/projects.rb
FactoryBot.define do factory :project do sequence(:name) { |n| "Project #{n}" } description "A test project." due_on 1.week.from_now association :owner # ←ここでuserインスタンス生成。:noteではこのインスタンスを使う end end
関連付けを別名で行なっている時の注意点
モデルにおいて、他データとの関連付けを下記のように行なっている場合、
app/models/project.rb
class Project < ApplicationRecord # ... belongs_to :owner, class_name: User, foreign_key: :user_id # ... end
下記のように別名でインスタンスを作成するためには、
factory :project do # ... association :owner end
該当のファクトリにaliasを指定する
factory :user, aliases: [:owner] do # ... end
継承とtraitで複雑なファクトリを簡単にする
属性値が異なるファクトリを複数作成したい場合などに、
継承とtraitを用いることで記述を簡易化し、可読性を高めることができる。
ファクトリを複数作るやり方(重複ありで読みにくい)
締め切りが異なるプロジェクトを作成したい場合、
インスタンスの名称を変更し、クラス名を渡せば良い。
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner end # 昨日が締め切りのプロジェクト factroy :project_due_yesterday, class: Project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.day.ago association :owner end end
しかし、上記の実装だと、重複している箇所が多くDRYになっていない。
そのため、継承またはtraitを使って重複を解消する。
ファクトリの継承を使う
継承を使うと class: Project の指定は不要。
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner # 昨日が締め切りのプロジェクト factroy :project_due_yesterday do due_on 1.day.ago end end end
traitを使う
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner # 昨日が締め切り trait :due_yesterday do due_on 1.day.ago end end end
トレイトの使い方は下記のように、引数で渡す。
describe "latestatus" do # 締切日が過ぎていれば遅延していること it "is late when the due date is past today" do project = FactoryBot.create(:project, :due_yesterday) expect(project).to be_late end end
コールバック
ファクトリがオブジェクトをcreateする前後でアクションを追加できる。
trait :with_notes do after(:create) { |project| create_list(:note, 5, project: project) } end
after(:create)の他には、after(:build)やbefore(:create)などもある
ファクトリを安全に使う
不要データを作成したり、パフォーマンス面を考慮し、下記に従う。
- コールバックを使って関連データを作成する場合、トレイトの中でセットアップする
- 可能な限り、FactoryBot.createではなくFactoryBot.buildを使う
- 最初の頃はファクトリを使う必要があるのか?を自問する