Help us understand the problem. What is going on with this article?

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

metheglin
敷布団カバーと掛布団カバーを逆につけて寝ています
https://metheglin.jp/#logical
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away