おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

factory_girl で関連先の属性を動的に設定して作成する方法

下記のような UserGroup, User, Blog があるとします。

# == Schema Information
#
# Table name: user_groups
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class UserGroup < ActiveRecord::Base
end

# == Schema Information
#
# Table name: users
#
#  id            :integer          not null, primary key
#  name          :string(255)
#  user_group_id :integer
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#

class User < ActiveRecord::Base
  has_many :blogs
  belongs_to :user_group
end

# == Schema Information
#
# Table name: blogs
#
#  id         :integer          not null, primary key
#  name       :string(255)
#  user_id    :integer
#  user_group :integer
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Blog < ActiveRecord::Base
  belongs_to :user
  belongs_to :user_group
end

これらの factory_girl の定義を書いてみましょう。素直に書くと下記のようになりました。

FactoryGirl.define do
  factory :user_group do
    sequence(:name) { "グループ#{i}"}
  end
end

FactoryGirl.define do
  factory :user do
    sequence(:name) { |i| "ユーザ#{i}" }
    user_group
  end
end

FactoryGirl.define do
  factory :blog do
    sequence(:name) { |i| "ブログ#{i}" }
    user_group
    user
  end
end

この状態でFactoryGirl.create(:blog)のようにすると、UserGroup と User のレコードも一緒に作成されます。これは便利なのですが、上記の定義だと Blog の作成をする時と User の作成をする時で二つの UserGroup が作成されてしまいます。これはよくありませんね。BlogとUserは同じUserGroupを参照しているのが望ましいです。

下記のように書ければよいのですが、これでFactoryGirl.create(:blog)とすると FactoryGirl::AttributeDefinitionError: Attribute already defined: user_groupとなってしまいます。現状のFactoryGirlでは関連の属性を動的に設定することは出来ないようです(静的な値であれば設定できます)。

FactoryGirl.define do
  factory :blog do
    sequence(:name) { |i| "ブログ#{i}" }
    user_group
    association :user, factory: user, user_group: user_group
  end
end

そこで下記のようにして対応します。

FactoryGirl.define do
  factory :blog do
    sequence(:name) { |i| "ブログ#{i}" }
    user_group
    user { create(:user, user_group: user_group) }
  end
end

これでFactoryGirl.create(:blog)としたときに UserGroup が作られるのは一度だけになりました。ただし、この書き方をすると、FactoryGirl.build(:blog)としたときにも User と UserGroup が作られてしまいます。FactoryGirl.buildも使いたいときには build 用の定義を別途作ってあげる必要がありそうですね。

factory_girl を初めとした Fixture Replacement は関連が複雑なときは定義がけっこう大変な印象です。とはいえ今更Fixturesには戻れないのですが…。

参考

ruby on rails - Pass parameter in setting attribute on association in FactoryGirl - Stack Overflow