Edited at

FactoryBot(旧FactoryGirl)で関連データを同時に生成する方法いろいろ

More than 1 year has passed since last update.

リレーションの関係にある複数のモデルを連動して追加したいときの方法いろいろ.

リレーション関係なくても、インスタンス生成と同時に特定の処理を実行したいときにも使える.


呼び出し側でcreateにブロック渡す

rspecなどのテストコード側でカスタムに関連データを追加する方法.

shop has_many staffsのリレーションができている前提で、以下のようにbuildcreateにブロックを渡せば、ブロック内で生成されたインスタンスを自由に修正できる.

# 

# 生成されたインスタンスの内容をブロック内で自由に修正できる
#
shop = FactoryBot.build(:shop) do |s|
s.name = "あいうえお"
end

#
# 永続化せずにインスタンス生成
# shopにひもづくstaffをブロック内で追加している
#
shop = FactoryBot.build(:shop) do |s|
s.staffs.build(FactoryBot.attributes_for(:staff))
end

#
# 永続化されたインスタンス生成
# shopにひもづくstaffをブロック内で追加している
#
shop = FactoryBot.create(:shop) do |s|
s.staffs.create(FactoryBot.attributes_for(:staff))
end


ファクトリ内でファクトリ呼び出し

ファクトリ内で別のファクトリ呼び出すと連鎖的にインスタンス生成することができる.

モデルが互いに関連していた場合はリレーションもはられた状態になる.

以下の例は、staff belongs_to shopのリレーションができている前提で動作する.

FactoryBot.define do

factory :staff, class: Staff do
name "Isaac Newton"
role "physicist"

shop # ファクトリよびだし
end

factory :shop, class: Shop do
name "テストショップ"
end
end

staff = FactoryBot.create :staff

# staffおよびshopがinsertされる
staff.shop
# 連鎖生成されたshopをアソシエーションたどって参照できる

おそらく、↑ような別のファクトリを呼び出す記法は以下の省略形?になる.

FactoryBot.define do

factory :staff, class: Staff do
name "Isaac Newton"
role "physicist"

shop { FactoryBot.create :shop }
end
end

↑の書き方だと、ブロックを使えるので自由度の高い生成ができるが、こういったカスタマイズしたい場合は次に紹介のassociationを使うのが推奨らしい.

また、trait使えばアソシエーション先を作成するかどうか切り替えることもできる

FactoryBot.define do

factory :staff, class: Staff do
name "Isaac Newton"
role "physicist"

trait :with_shop do
shop
end
end

factory :shop, class: Shop do
name "テストショップ"
end
end

# 

# アソシエーション先を作らない
#
user = FactoryBot.build :staff
user.shop
# nil

#
# アソシエーション先作る
#
user = FactoryBot.build :staff, :with_shop
user.shop
# => #<Shop id: 1, name: "テストショップ">


Association

staff belongs_to shopの関連がある前提で以下のファクトリが動作する.

associationメソッドにアソシエーション名とファクトリ名を指定する.

任意で生成時の属性を指定することもできる.

factory :staff, class: Staff do

name "Isaac Newton"
association :shop, factory: :shop, name: "マダガスカルショップ"
end

factory :shop, class: Shop do
name "テストショップ"
end

staff = FactoryBot.create :staff

staff.shop.name # マダガスカルショップ

ただし、associationはhas_manyリレーションでは使えない.

has_manyで関連データをつっこむときは次に紹介のcallbackを使う.

これも先ほどと同様trait使えばassociation実行切り替えもできる.

FactoryBot.define do

factory :staff, class: Staff do
name "Isaac Newton"
role "physicist"

trait :with_shop do
association :shop, factory: :shop, name: "マダガスカルショップ"
end
end

factory :shop, class: Shop do
name "テストショップ"
end
end


Callback

callbackを使えば、生成したインスタンスがcreate, buildされたイベントの直後に自由にインスタンスを修正することができる.

以下は、shop has_many staffsの関連ができている前提で動作.

factory :shop, class: Shop do

name "テストショップ"
after(:create) do |shop|
shop.staffs << FactoryBot.create(:staff, name: "織田信長")
shop.staffs << FactoryBot.create(:staff, name: "葛飾北斎")
end
end

↑の例ではbuild時は関連が作成されない.

なのでcallback作る際はafter createではなく、after buildにしておいたほうがよさそう.

after buildコールバックはFactoryBot.createした後も実行される.

factory :shop, class: Shop do

name "テストショップ"
after(:build) do |shop|
shop.staffs << FactoryBot.build(:staff, name: "織田信長")
shop.staffs << FactoryBot.build(:staff, name: "葛飾北斎")
end
end

shop = FactoryBot.build :shop
shop.persisted? # false
shop.users.first.persisted? # false

こちらももちろんtrait使えばコールバック実行切り替えできる.

以下の例ではデフォルトでデフォルトのtraitを実行して、カスタムのtraitが指定された場合はデフォルトのデータを上書きする.

factory :shop, class: Shop do

name "テストショップ"

with_default_staffs

trait :with_default_staffs do
after(:build) do |shop|
shop.staffs << FactoryBot.build(:staff, name: "one")
shop.staffs << FactoryBot.build(:staff, name: "two")
end
end

trait :with_japanese_staffs do
after(:build) do |shop|
shop.staffs = []
shop.staffs << FactoryBot.build(:staff, name: "織田信長")
shop.staffs << FactoryBot.build(:staff, name: "葛飾北斎")
end
end

trait :with_english_staffs do
after(:build) do |shop|
shop.staffs = []
shop.staffs << FactoryBot.build(:staff, name: "Isaac Newton")
shop.staffs << FactoryBot.build(:staff, name: "Robert Hooke")
end
end
end

FactoryBot.create :shop

# アソシエーション先としてdefault staffsが作成される
FactoryBot.create :shop, :with_japanese_staffs
# アソシエーション先としてjapanese staffsが作成される
FactoryBot.create :shop, :with_english_staffs
# アソシエーション先としてenglish staffsが作成される


References