サンプルコードとして、ProjectモデルとUserモデルを例にします。
モデルの実装は以下です。
class Project < ApplicationRecord
has_many :users
end
class User < ApplicationRecord
belongs_to :project
end
このように関連のあるモデルで、片方を作成すれば自動的にもう片方も作成したいときの方法を記載します。
belongs_to
基本形は以下です。
FactoryBot.define do
factory :user do
association :project
end
end
FactoryBot.define do
factory :project
end
ProjectとUserに対応するfactoryをそれぞれ作成し、User側のfactoryにassociation :project
と記載します。これで以下のようにテストデータを作成すると、
FactoryBot.create(:user) # UserとProjectを作成して保存
Userと一緒にProjectも作成されます。
以下のようにcreateではなくbuildの場合、
FactoryBot.build(:user) # UserとProjectを作成(保存されない)
UserとProjectはメモリ上に作成され保存はされません。
もちろんsaveすると両方DBに保存されます。
user = FactoryBot.build(:user)
user.save # UserとProjectの両方が保存される
このように親側であるUserの状態と連動するので便利です。
associationのオプション
associationにはオプションがいろいろあります。
# 基本形
association :project
# association :project の省略形。factory名と関連名が同じ場合にこう書ける。
project
# factoryを指定することも可能。
association :project, factory: :sub_project
# Projectの属性を上書きできる。
association :project, project_name: 'プロジェクトA'
associationの上書き
User作成時に自動で作成されるProjectとは別のProjectを使いたい場合、create時に属性の上書きをすることが可能です。
my_project = Project.create(name: '私のプロジェクト')
FactoryBot.create(:user, project: my_project)
associationが使えないとき
そもそもモデルにbelongs_toが定義されていないけど実際には関連があって一緒に作成したい場合など(そんな時ある?って感じですが私のプロジェクトではある…)
属性のブロックにはロジックも書けるので、こんなふうにすることもできます。
project_id { Project.create(name: 'my project').id }
たとえばProjectがマスタデータのようなもので、seedなどで事前にデータを用意できる場合、以下のようにIDを指定することもあります。
project_id { 1 }
# ID指定に抵抗がある場合は、定数にしておくと良いかも
project_id { ProjectData::MY_PROJECT_ID }
必要なすべてのデータをrspecの中で作成するのは大変なので、マスタデータは本番環境と同じデータをseedやfixtureで事前に作成しておくのがおすすめです。
belongs_to optional:true の場合
UserはProjectへの所属が必須ではないとき(project_idのnullを許可する場合)
class User < ApplicationRecord
belongs_to :project, optional: true
end
以下のようにfactoryを分けることができます。
FactoryBot.define do
factory :user do
factory :user_with_project do
association :project
end
end
end
FactoryBot.create(:user) # Projectは作成されない
FactoryBot.create(:user_with_project) # Projectも作成される
factoryをネストしていますが、これはfactoryの継承の書き方です。
続いて別の方法で、以下のようにtraitを使う方法もあります。
FactoryBot.define do
factory :user
trait :with_project do
association :project
end
end
FactoryBot.create(:user) # Projectは作成されない
FactoryBot.create(:user, :with_project) # Projectも作成される
traitを使った方法は便利ですが、FactoryBot.create(:user, :with_project, :with_〇〇, :with_△△, :with_□□...)
のようにtraitだらけになりがちです。
factoryを使うからには、整合性の取れたデータを提供した方が良いと思うので、traitを好きに組み合わせてね〜という設計よりは、想定される整合性の取れた状態のfactoryを提供する方が良いのではないかと個人的には思います。
has_many
Projectを作成したときに所属するUserを一緒に作成する場合
FactoryBot.define do
factory :project, traits: [:with_users] do
trait :with_users do
transient do
users_count { 0 }
end
after(:build) do |project, evaluator|
evaluator.users_count.each do
project.users << FactoryBot.build(:user, project: project)
end
end
end
end
end
FactoryBot.create(:project) # Userが一緒に作成されない
FactoryBot.create(:project, users_count: 5) # Userが5件作成される
afterはfactoryのコールバックで、この場合にはprojectをbuildしたあとに呼び出されます。
users_countで指定した数分、Userを作成します。デフォルトではUserは作成しません。
多対多(has_many through)の場合
Projectは複数のCategoryに所属できるコードを例にします。
モデルの実装は以下です。
class Project < ApplicationRecord
has_many :categories, through: project_categories
has_many :project_categories
end
class ProjectCategory < ApplicationRecord
belongs_to :project
belongs_to :category
end
class Category < ApplicationRecord
end
先ほどと同じように、trait、transient、afterコールバックを利用して実装できます。
FactoryBot.define do
factory :project, traits: [:with_categories] do
trait :with_categories do
transient do
my_categories { [] }
end
after(:build) do |project, evaluator|
evaluator.my_categories.each do |category_id|
project.categories << FactoryBot.build(:project_category, category_id: category_id, project_id: project.id)
end
end
end
end
end
FactoryBot.create(:project) # Categoryに所属なし
FactoryBot.create(:project, my_categories: [1, 2]) # Categoryにふたつ所属
この例ではCategoryのデータは事前にseedで登録済みですので、IDを直接しています。
所属するカテゴリをfactoryの呼び出し側が自由に設定できて便利です。もちろん、整合性を考慮して自由に設定させたくなければ、factory内で閉じた実装をすると良いと思います。
belongs_toとhas_manyの循環に注意
User側からもProject側からもお互いを作成するようにfactoryを書いた場合、片方を作成するとお互いを作成しつづけ、無限ループになってしまいます。
これを回避するには、factory内でFactoryBot.build
やFactoryBot.create
を利用せず、User.new
やUser.create
を使う…などでしょうか。
私のプロジェクトの場合は、has_manyをfactoryで実装する場合は、シンプルな多対多関係のとき、上記の例だとProjectとCategoryのような関係のときだけにしています。カテゴリはチェックボックスでCategory側に実装がほぼなく、Project側がテストのメインだ、という場合です。
逆にUserやProjectのような、どちらにもしっかりと実装がある場合は、factoryでhas_manyを実装しないルールにしています。
こんな回避方法もあるようです。
https://qiita.com/roba4coding/items/849ff18c4f57d3c4aff8
FactoryBotにこだわる必要はない
とても便利なFactoryBotですが、一方でfactoryのassociationをどんどんだとって無駄にたくさんのデータを作成してしまうこともあります。最小限のデータでよいときは、FactoryBotを使わずにモデルの機能だけでデータを作成しても良いと思います。テストの局面に応じて適切に使い分けていきたいです。