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.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.