独自の構築

factory_botを使って、initializeに渡される属性があるオブジェクトを構築したいときや、クラスを構築する上で単にnewを呼ぶ以上のことをしたいときは、ファクトリにinitialize_withを定義して既定の挙動を上塗りできます。 例えば以下です。

# user.rb
class User
  attr_accessor :name, :email

  def initialize(name)
    @name = name
  end
end

# factories.rb
sequence(:email) { |n| "person#{n}@example.com" }

factory :user do
  name { "Jane Doe" }
  email

  initialize_with { new(name) }
end

build(:user).name # Jane Doe

factory_botはActiveRecordで飛び抜けて上手く書けるようになっていますが、どんなRubyクラスでも動作できます。 ActiveRecordと最大限の互換性があるよう、既定の初期化器はクラスを構築するのに実引数なしでnewを読んで全てのインスタンスを構築します。 それから属性の書込みメソッドを呼んで全ての属性値を代入します。 ActiveRecordでは上手く動きますが、他のRubyのクラスのほとんどは実際にはうまくいきません。

以下の目的で初期化器を上塗りできます。

  • initializeに実引数が必須の非ActiveRecordオブジェクトを構築する。
  • newではないメソッドを使ってインスタンスをインスタンス化する。
  • 構築された後にインスタンスを修飾するような雑なことをする。

initialize_withを使うとき、newを呼ぶときにクラス自体を宣言する必要はありません。 しかし呼びたいその他のクラスメソッドは明示的にクラスに対して呼ばなければなりません。

例は以下です。

factory :user do
  name { "John Doe" }

  initialize_with { User.build_with_name(name) }
end

attributesを呼んでinitialize_withブロック内で全ての公の属性を使うこともできます。

factory :user do
  transient do
    comments_count { 5 }
  end

  name "John Doe"

  initialize_with { new(**attributes) }
end

こうするとnewに渡される全ての属性のハッシュを構築します。 一過的属性は含まれませんが、ファクトリで定義されたその他全て(関連、評価された系列など)が渡されます。

FactoryBot.defineブロック内に含めると全てのファクトリにinitialize_withを定義できます。

FactoryBot.define do
  initialize_with { new("Awesome first argument") }
end

initialize_withを使うとき、initialize_withブロック内で使う属性は構築子でのみ代入されます。 これは以下のコードと大まかに同じです。

FactoryBot.define do
  factory :user do
    initialize_with { new(name) }

    name { 'value' }
  end
end

build(:user)
# ……とすると以下が実行されます。
User.new('value')

これは重複する代入を防止しています。 4.0より前のfactory_botでは以下が走っていました。

FactoryBot.define do
  factory :user do
    initialize_with { new(name) }

    name { 'value' }
  end
end

build(:user)
# ……とすると以下が実行されます。
user = User.new('value')
user.name = 'value'