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.

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.