Factory Bot Callbacks

Dan Croak

Factory Bot now has callbacks thanks to Nate Sutton.

this shit is bananas

There are three callbacks:

after(:build)
after(:create)
after(:stub)

When build(:user) is invoked, the after(:build) callback runs after building the user. Likewise for create and stub.

These come in handy in a number of common use cases.

Basic has many associations

Models:

class Article < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :article
end

Factories:

factory :article do
  body { 'password' }

  factory :article_with_comment do
    after(:create) do |article|
      create(:comment, article: article)
    end
  end
end

factory :comment do
  body { 'Great article!' }
end

Nice. Callbacks let us do this:

article = create(:article_with_comment)

Instead of this:

article = create(:article)
create(:comment, article: article)

Polymorphic relationships

The savings get larger when the object graph gets more complex:

Models:

class User < ActiveRecord::Base
  has_many :interests, as: :interested
  has_many :topics, through: :interests
end

class Interest < ActiveRecord::Base
  belongs_to :topic
  belongs_to :interested, polymorphic: true
end

Building block factories:

factory :user
  email
  password { 'password' }

  factory :email_confirmed_user do
    email_confirmed { true }
  end
end

factory :topic do
  name { 'topic_name' }
end

factory :interest
  topic
  interested factory: :user
end

factory :music_interest, class: 'Interest' do
  topic.association(:topic, name: 'Music')
end

factory :sports_interest, class: 'Interest' do
  topic.association(:topic, name: 'Sports')
end

And now the payoff:

factory :musical_user, parent: :email_confirmed_user do
  after(:create) { |user| create(:music_interest, interested: user)
end

factory :sporty_user, parent: :email_confirmed_user do
  after(:create) { |user| create(:sports_interest, interested: user) }
end

Again, factories let us do this:

user = create(:musical_user)

Instead of:

user = create(:email_confirmed_user)
create(:music_interest, interested: user)

More intention-revealing. More encapsulation, protecting us from change.

Working with fakes

Fakes are a good approach for testing objects that interface with web services such as geocoding and payment processing.

class User < ActiveRecord::Base
  acts_as_mappable
  before_validation :geocode_location, if: :location_changed?

  def geocode_location
    geo = Geokit::Geocoders::MultiGeocoder.geocode(location)
    self.lat, self.lng = geo.lat, geo.lng
  end
end

factory :user do
  factory :boston_user do
    location { 'Boston, MA' }

    after(:build) do |user|
      Geokit::Geocoders::FakeGeocoder.locations['Boston, MA'] = [0, 1]
    end
  end
end

Enjoy

Install:

gem 'factory_bot'

Happy testing.


Disclaimer:

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