Get Your Callbacks On with Factory Bot 3.3

Josh Clayton

FactoryBot 3.3.0 was released this weekend with a slew of improvements.

To install, add (or change) your Gemfile:

gem 'factory_bot_rails', '~> 3.3.0'

New Callback Syntax

Callbacks have been revamped to work well in conjunction with custom strategies. Instead of declaring callbacks like this:

FactoryBot.define do
  factory :user do
    factory :user_with_posts do
      after(:create) {|instance| create_list(:post, 5, user: instance) }
    end
  end
end

you can declare callbacks with before and after, passing the symbol of the callback as the name:

FactoryBot.define do
  factory :user do
    after(:custom) {|instance| instance.do_something_custom! }

    factory :user_with_posts do
      after(:create) {|instance| create_list(:post, 5, user: instance) }
    end
  end
end

Finally, you can use completely custom callbacks without a before or after prepended by just calling callback:

FactoryBot.define do
  factory :user do
    callback(:custom_callback) {|instance| instance.do_something_custom! }
  end
end

These work great with custom strategies:

class CustomStrategy
  def initialize
    @strategy = FactoryBot.strategy_by_name(:create).new
  end

  delegate :association, to: :@strategy

  def result(evaluation)
    @strategy.result(evaluation).tap do |instance|
      evaluation.notify(:custom_callback, instance) # runs callback(:custom_callback)
      evaluation.notify(:after_custom, instance)    # runs after(:custom)
    end
  end
end

Support all *_list methods

FactoryBot already introduced build_list and create_list to build and create an array of instances; in 3.3.0, *_list methods are generated dynamically for all strategies registered, so build_stubbed_list and attributes_for_list join the immediate roster of methods; if you were to register a strategy named “insert”, insert_list would exist as well.

Fix to_create and initialize_with within traits

Traits are a great way to name an abstract concept of attributes, but for a long time, they didn’t support defining to_create or initialize_with. 3.3.0 fixes this shortcoming by having to_create and initialize_with behave in traits exactly as you’d expect. This is perfect for decorating objects from within FactoryBot.

class NotifierDecorator < BasicObject
  undef_method :==

  def initialize(component)
    @component = component
  end

  def save!
    @component.save!.tap do
      Notifier.new(@component).notify("saved!")
    end
  end

  def method_missing(name, *args, &block)
    @component.send(name, *args, &block)
  end

  def send(symbol, *args)
    __send__(symbol, *args)
  end
end

FactoryBot.define do
  trait :with_notifications do
    to_create {|instance| NotifierDecorator.new(instance).save! }
  end

  factory :user
end

create(:user, :with_notifications) # decorates save! when the instance is created

FactoryBot.define do
  trait :with_notifications do
    initialize_with { NotifierDecorator.new(new) }
  end

  factory :post
end

create(:post, :with_notifications) # returns a post instance decorated with NotifierDecorator

Define to_create and initialize_with globally

If you’re using an ORM other than ActiveRecord, you may want to call different methods for persistence. Declaring a to_create (or initialize_with, if you wanted to use a global decorator) within the FactoryBot.define block will now apply to all declared factories, behaving much like sequences, traits, and factories.

You can override the global to_create or initialize_with with traits or by defining to_create in a factory explicitly.

FactoryBot.define do
  to_create {|instance| instance.persist! }

  factory :user do
    factory :user_backed_by_active_record do
      to_create {|instance| instance.save! }
    end
  end
end

What’s next

There are still cases where traits don’t behave correctly (using implicit traits is a big remaining bug) and more work for initialize_with and accessing attributes needs to be done.


Disclaimer:

Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.