GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Waiting For a FactoryGirl

I Can't Get No Satisfaction (From Fixtures)

Here at thoughtbot, we've had it with fixtures. Is Susie an admin? Which user owns the Exciting Test post? Are there any categories without posts, or should I add that fixture for this test? How did this post end up in the future? Do you like asking these questions when writing tests? I don't.

I also don't like tests that don't tell you anything about the context you're testing:

should "find recently updated posts" do
  assert_equal posts(:lions_attack), Post.most_recent
end

One method in one model being tested, and three files to look through to understand it. I'll pass, thank you.

I'm Moving On (To Factories)

After being introduced to factories by various blogs and coworkers, I looked for a plugin to get me started. I tried out object daddy and a couple others, but none of them quite scratched that itch I needed to reach. Some had questionable implementations, some had poor (or no) tests themselves, and none of them supported everything we wanted: a nice definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and potentially mock objects), and support for multiple factories for the same class (user, admin_user, and so on).

Eventually, I ended up just writing little methods that I included in Test::Unit::TestCase:

def create_post (attribs = {})
  attribs = {
    :title     => 'goodbye, fixtures',
    :approved => true
  }.update(attribs)
  attribs[:author] ||= create_user
  Post.create!(attribs)
end

It got the job done, and I was finally free of fixtures, but my factory definitions were hard to follow, and became repetitive pretty fast. After discussing the pros and cons of the various implementations we'd tried, several thoughtbotters and I wrote out our ideal syntax for defining and using factories, and this weekend that theoretical syntax became a reality.

She's a Rainbow

Introducing factory_girl:

# test/test_helper.rb

require 'factory_girl'

# Let's define a sequence that factories can use.  This sequence defines a
# unique e-mail address.  The first address will be "somebody1@example.com",
# and the second will be "somebody2@example.com."
Factory.sequence :email do |n|
  "somebody#{n}@example.com"
end

# Let's define a factory for the User model. The class name is guessed from the
# factory name.
Factory.define :user do |f|
  # These properties are set statically, and are evaluated when the factory is
  # defined.
  f.first_name 'John'
  f.last_name  'Doe'
  f.admin      false
  # This property is set "lazily." The block will be called whenever an
  # instance is generated, and the return value of the block is used as the
  # value for the attribute.
  f.email      { Factory.next(:email) }
end

Factory.define :post do |f|
  f.title    'undef toggle!'
  f.approved true
  # Lazy attribute blocks are passed a proxy object that can be used to
  # generate associations lazily. The object generated will depend on which
  # build strategy you're using. For example, if you generate an unsaved post,
  # this will generate an unsaved user as well.
  f.author   {|a| a.association(:user) }
end

# Let's define a factory with a custom classname:
Factory.define :admin_user, :class => User do |f|
  f.first_name 'Billy'
  f.last_name  'Idol'
  f.email      { Factory.next(:email) }
  f.admin      true
end

These factories can be used like so:

# test/post_test.rb

class PostTest < Test::Unit::TestCase

  should "only find approved posts" do
    # Generate and save some Post instances
    Factory(:post, :approved => false)
    Factory(:post, :approved => true)

    posts = Post.approved
    assert posts.all? {|p| p.approved? }
  end

  context "a post without a title" do

    setup do
      # Build a post object
      @post = Factory.build(:post, :title => '')
    end

    should "not be valid" do
      assert !@post.valid?
    end

  end

end

Combined with Shoulda's contexts, factory_girl makes tests readable, DRY, and explicit. Until I find another itch to scratch, I'm in testing heaven.

You Better Move On

Want to try it out for yourself? factory_girl is available on github. You can also install it using RubyGems: <pre>$ sudo gem install thoughtbot-factory_girl --source=http://gems.github.com</pre>

Also, make sure to check out the rdoc.

Update: Do you have questions or comments on factory_girl? Feel free to post them on the new mailing list.

Happy testing!