Waiting For a Factory Girl
Posted by Joe Ferris
Jun 06
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:1 2 3 |
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:1 2 3 4 5 6 7 8 |
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:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# 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:$ sudo gem install thoughtbot-factory_girl --source=http://gems.github.com
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!
Comments on this post
Jun 06
Chris O'Sulivan said,
Woo hoo!
Jun 06
Justin Marney said,
Have you taken a look at http://replacefixtures.rubyforge.org/ it doesn pretty much the same thing as factory_girl. We’ve been using for some time to rid ourselves of fixtures.
Jun 06
Lucas Húngaro said,
Yay!
Shoulda and factory_girl: BDD is now better than ever!
Jun 06
Tammer Saleh said,
@Justin – I had looked at fixture_replacement a while ago, and there were a couple of things that I didn’t really like about it. One was that it doesn’t seem to have any special capability to handle associations (like lazy evaluation), and the other is that it sidesteps the generator issue with random values. There’s always a chance of collision there, and it’s harder to ensure that the random values will pass your validations.
Jun 06
Sean Cribbs said,
I highly recommend scenarios, which are drop-in replacement for fixtures, plus fixture dependencies, plus test/spec helper methods, all rolled into one!
Jun 06
BrianTheCoder said,
Check out can_has_fixtures too. It works similar to this, but works better with you models themselves. It’s on github. http://github.com/benburkert/can_has_fixtures/tree/master
Jun 06
Eric Mill said,
@Sean, how would you compare scenarios to factory_girl?
Jun 06
Tammer Saleh said,
@sean: I know quite a few people using fixture scenarios, but they aren’t quite what we’re looking for. We don’t want to have to bounce between multiple files to figure out the context of a test. We’d rather see the records being built up inside the test files themselves.
Jun 07
Yossef said,
It’s always good to see someone else come to the magic realization. And more fixture replacements/factories/generators/whatever are a good thing. In the end some will win, and they’ll all be better for the competition.
As one of the developers (though not the progenitor) of object_daddy, I was trying to see where it fell on your list of failures. At the very least, I know there are tests. ;)
Personally, I’m not a big fan of the “proper” Factory pattern. Rick’s agreement is why OD puts more methods on the models themselves rather than creating a new class to contain all of this logic. While this is largely a question of syntax and somewhat akin to me disliking `assert` and preferring RSpec’s `should`, it does make some ideas more difficult. Multiple generator groups for a class has come up so that, for example, it would be simple to generate a normal User or an admin User. So far there’s been very little call for anything like that and we’re waiting until it becomes a pain point. When that time comes, it’s going to require more work than it does with a different Factory scheme.
Jun 07
bryanl said,
At one point, I thought I needed more than the rails scenario plugin. Now I find it is more than adequate.
Bouncing to another file to see the context test can be somewhat avoided if you use descriptive names for your scenarios.
I hope you and Croaky find what you are looking for.
Jun 07
eddie said,
very cool code snippets.. Thanks for sharing
Jun 10
Wallen said,
f.name “Foo” seems to raise an error (wrong number of arguments 1 for 0)
You have to call f.add_attribute :name, “Foo”
Only for name, the rest of them seem to work fine. Slightly annoying since 80% of my models have a name method. Not the end of the world.
Also, in your RDoc for method_missing you have:but it should be :name instead of :user in the f.add_attribute
Excellent work!!
Jun 10
Joe Ferris said,
@Wallen: I’ve run into that particular gotcha while using factory_girl myself, and I’m planning on renaming Factory#name to Factory#factory_name to avoid that common conflict.
Also, thanks for catching that mistake in the docs.
-Joe
Jun 10
Matt Hooks said,
You guys make me happy.
Jun 11
Christoph said,
Hey, this is really nice – I imedeately started using it!
One thing I noticed it doesn’t recognize :my_class as MyClass, which shouldn’t be difficult using the rails methods.
Thanks for your work, and keep it up!
Jun 11
David Lowenfels said,
Nice work Joe! I have a very similar project at http://github.com/dfl/factories-and-workers/
It mixes-in to Test::Unit::TestCase so there is no namespacing off of Factory required. Also, it uses plain old hash options, so it requires a lambda for lazy evaluation.
The method syntax in factory-girl is kind of nice, but I would use method_missing to delegate methods to the block variable… that would visually clean things up a bit.
Jun 21
Gustav Paul said,
Nice! I’ve been using this for about 20 minutes and already its super fun! It rocked being able to just open the Factory class and pop in some custom methods to use on #define’s iterator.
Have you thought of adding a :like => :user option on #define?
Inheriting all the :user defaults and its class from the already defined `Factory.define(:user)` I’ll see about hacking a patch if you’re interested?
Jun 24
aspire bank said,
Nice Site! http://google.com
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives