前回の続き
テストが複雑になってもテストデータのセットアップはシンプルにしたい。
そんなときに使えるRubyライブラリとして有名なものにGemのFactory Botがあるので、使い方や注意点についてまとめていきます。
#ファクトリとフィクスチャ
Railsでサンプルデータを生成する手段として、フィクスチャと呼ばれる機能がYAML形式のファイルとしてデフォルトで提供されています。フィクスチャは優れた機能ですが、Railsがフィクスチャのデータをデータベースに読み込む際にActive Recordを使わないため、本番環境の挙動とは異なるといった注意すべき点もあります。
ファクトリは簡単にテストデータを生成でき、コードも短く読みやすくすることができますが、生成されるデータを意識して記述しないと、テスト中に予期しないデータが生成されたり、テストの実行が無駄に遅くなったりします。
それぞれの特徴を理解した上で使い分ければ、よりスムーズにテストを書くことができるようになります。
#Factory Botインストール
group :development, :test do
gem "factory_bot_rails"
end
$ bundle install
ついでに、rails generateでモデルを生成する際にファクトリも自動で生成されるように設定していきます。
fixtures: falseを削除するだけです。
config.generators do |g|
g.test_framework :rspec,
view_specs: false
helper_specs: false,
routing_specs: false
end
#ファクトリの追加
Factory Botをインストールできたので、Userモデルのファクトリを追加してみましょう。
$ rails g factory_bot:model user
このコマンドを実行すると、specディレクトリ内にfactoriesという新しいディレクトリが作られ、その中にはusers.rbという名前のファイルが以下のような内容で作られます。
FactoryBot.define do
factory :user do
end
end
ここにテストデータを作っていきます。
FactoryBot.define do
factory :user do
name { "Zeisho" }
email { "hoge@hoge.com" }
password { "hogehoge" }
end
end
ちゃんとテストデータが作れているか、user_spec.rbで確認してみましょう。
require 'rails_helper'
describe User do
#有効なファクトリを持つこと
it "has a valid factory" do
expect(FactoryBot.build(:user)).to be_valid
end
#他のスペック群
end
ここでは、FactoryBot.buildを使ってユーザーをインスタンス化し有効性をテストしています。前回の記事で記述した有効なユーザーインスタンスのテストよりコンパクトなスペックになりました。
FactoryBotのデータを上書きしてバリデーションエラーのテストを書き換えていきます。
require 'rails_helper'
describe User do
#有効なファクトリを持つこと
it "has a valid factory" do
expect(FactoryBot.build(:user)).to be_valid
end
#名前がなければ無効な状態であること
it "is invalid without a name"
user = FactoryBot.build(:user, name: nil)
user.valid?
expect(user.errors[:name]).to include("can't be blank")
end
#メールアドレスがなければ無効な状態であること
it "is invalid without a email"
user = FactoryBot.build(:user, email: nil)
user.valid?
expect(user.errors[:email]).to include("can't be blank")
end
#パスワードがなければ無効な状態であること
it "is invalid without a password"
user = FactoryBot.build(:user, password: nil)
user.valid?
expect(user.errors[:password]).to include("can't be blank")
end
#重複したメールアドレスなら無効な状態であること
it "is invalid with a duplicate email address" do
FactoryBot.create(:user, email: "example@example.com")
user = FactoryBot.build(:user, email: "example@example.com")
user.valid?
expect(user.errors[:email]).to include("has already been taken")
end
end
#シーケンスでユニークなデータを生成する
example内で複数のテストデータを生成する際、
FactoryBot.create(:user)
を繰り返し使うと、nameやemailなどの属性が全く同じになるため、バリデーションエラーでテストの実行が止まってしまう場合があります。
FactoryBotではシーケンスを使ってユニークバリデーションを持つデータを生成して解決できます。
FactoryBot.define do
factory :user do
name { "Zeisho" }
sequence(:email) { |n| "hoge#{n}@hoge.com" }
password { "hogehoge" }
end
end
上記のようにすることで、ファクトリで新しいユーザーを生成する度にユニークなメールアドレスにすることができます。
#ファクトリで関連を扱う
FactoryBotでは複数モデルのアソシエーション(関連づけ)を意識したデータを生成することもできるので、紹介します。
関連づけをすると、例えばまずは、Userモデル、Projectモデルに属したNoteモデルのデータを作りたいときなどに、Noteモデルのインスタンスを生成すれば、それに紐づいたUser, Projectモデルのデータも自動で生成してくれるようになります。
まずは、Userモデル、Projectモデルに属したNoteモデルのデータを作ります。
$ rails g factory_bot:model note
FactoryBot.define do
factory :note do
message { "My important note." }
association :project #テストデータ projectとの関連づけ
user { project.owner } #テストデータ userとの関連づけ
end
end
続いて、Userモデルに属し、Noteモデルを所有しているProjectモデルです。
$ rails g factory_bot:model project
FactoryBot.define do
factory :project do
sequence(:name) { |n| "Project #{n}" }
description { "A test project." }
due_on {1.week.from_now}
association :owner #owner(所有する)側の関連づけ
end
end
最後にUserにも関連づけの追記をすれば、完了です。
FactoryBot.define do
factory :user, aliases: [:owner] do
name { "Zeisho" }
sequence(:email) { |n| "hoge#{n}@hoge.com" }
password { "hogehoge" }
end
end
#ファクトリの継承
FactoryBotでは、1つのファイル内に複数のデータをすくることもでき、内容の重複する属性は記述を省略できます。
ユーザーに複数のプロジェクトを持たせる場合を例にみていきましょう。
FactoryBot.define do
factory :project do
sequence(:name) { |n| "Project #{n}" }
description { "A test project." }
due_on {1.week.from_now}
association :owner
#昨日が締め切りのプロジェクト
factory :project_due_yesterday do
due_on { 1.day.ago }
end
#今日が締め切りのプロジェクト
factory :project_due_today do
due_on { Date.current.in_time_zone }
end
#明日が締め切りのプロジェクト
factory :project_due_tomorrow do
due_on { 1.day.from_now }
end
end
end
:projectのブロック内に記述することで、:projectからdue_on以外の属性を継承したデータを生成できます。
テスト内でファクトリのデータを呼び出すときは、ファクトリ名をそのまま指定することで呼び出すことができます。
また、FactoryBotは、入れ子になっていることから:project_due_yesterday, :project_due_today, :project_due_tomorrowが:projectの子ファクトリであると判断するため、traitを使ってclass: Projectの指定もなくすことができます。
FactoryBot.define do
factory :project do
sequence(:name) { |n| "Project #{n}" }
description { "A test project." }
due_on {1.week.from_now}
association :owner
#昨日が締め切りのプロジェクト
trait :due_yesterday do
due_on { 1.day.ago }
end
#今日が締め切りのプロジェクト
trait :due_today do
due_on { Date.current.in_time_zone }
end
#明日が締め切りのプロジェクト
trait :due_tomorrow do
due_on { 1.day.from_now }
end
end
end
traitを使った子ファクトリを呼び出すには、
FactoryBot.create(:project, :due_yesterday)
のように、親ファクトリ, 子ファクトリとすることで呼び出せます。
#コールバック
コールバックを使うと、ファクトリがオブジェクトをcreateやbuildなどする前後に追加の処理を実行できます。
projectオブジェクトをcreateする際、それに紐づいたnoteを一緒に生成するコールバックを定義してみます。
FactoryBot.define do
factory :project do
sequence(:name) { |n| "Project #{n}" }
description { "A test project." }
due_on {1.week.from_now}
association :owner
#メモ付きのプロジェクト
trait :with_notes do
after(:create) { |project| create_list(:note, 5, project: project) }
end
end
end
:with_notesでは、projectオブジェクトをcreateした後に、create_listメソッドを使ってnoteオブジェクトを5つcreateしています。
定義したコールバックは下記のように呼び出すことができます。
FactoryBot.create(:project, :with_notes)