We're currently hiring Developers and Designers in Boston and New York.
Find out more and apply on our jobs page.

Nov 23

Ain't no calla back girl

Posted by dancroak

Factory Girl now has callbacks thanks to Nate Sutton (a.k.a. fowlduck).

this shit is bananas

There are three callbacks respectively called after Factory methods you already know:

after_build Factory.build
after_create Factory.create
after_stub Factory.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.define :article do |article|
  article.body { "password" }
end

Factory.define :comment do |comment|
  comment.body { 'Great article!' }
end

Factory.define :article_with_comment, :parent => :article do |article|
  article.after_create { |a| Factory(:comment, :article => a) }
end

Nice. Callbacks let us do this:

article = Factory(:article_with_comment)

Instead of this:

article = Factory(:article)
comment = Factory(: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.define :user do |user|
  user.email                 { Factory.next(:email) }
  user.password              { "password"   }
  user.password_confirmation { "password"   }
end

Factory.define :email_confirmed_user, :parent => :user do |user|
  user.email_confirmed { true }
end

Factory.define :topic do |topic|
  topic.name {'topic_name'}
end

Factory.define :interest do |interest|
  interest.association(:topic)
  interest.interested { |i| i.association(:user) }
end

Factory.define :music_interest, :class => 'Interest' do |interest|
  interest.topic { |topic| topic.association(:topic, :name => "Music") }
end

Factory.define :sports_interest, :class => 'Interest' do |interest|
  interest.topic { |topic| topic.association(:topic, :name => "Sports") }
end

And now the payoff:

Factory.define :musical_user, :parent => :email_confirmed_user do |user|
  user.after_create { |u| Factory(:music_interest, :interested => u) }
end

Factory.define :sporty_user, :parent => :email_confirmed_user do |user|
  user.after_create { |u| Factory(:sports_interest, :interested => u) }
end

Again, factories let us do this:

user = Factory(:musical_user)

Instead of:

user = Factory(:email_confirmed_user)
Factory(:music_interest, :interested => user)

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

Working with fakes

If you haven’t read it, read “Have you ever… faked it?”. We’re finding this to be 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.define :boston_user, :parent => :email_confirmed_user do |user|
  user.location { "Boston, MA" }

  user.after_build { |u|
    Geokit::Geocoders::FakeGeocoder.locations["Boston, MA"] = [0, 1]
  }
end

Enjoy!

Get version 1.2.3 with callbacks on Gemcutter:

sudo gem install factory_girl -s http://gemcutter.org

Happy testing.

blog comments powered by Disqus