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 It’s a Rainbow
Introducing factory_bot:
# test/test_helper.rb
require 'factory_bot'
# 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_bot 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_bot is available on github. You can also install it using RubyGems:
sudo gem install thoughtbot-factory_bot --source=http://gems.github.com
Also, make sure to check out the rdoc.
Update: Do you have questions or comments on factory_bot? Feel free to post them on the new mailing list.
Happy testing!
Disclaimer:
FactoryGirl was renamed to FactoryBot in 2017. Project name history can be found here.