The other day, purely by accident, I discovered that lurking behind our cute little factory girl tests was an apocolypse of angry zombies. “REALLY?,” you ask. Well no, not actual zombies.
But let me back up a second and explain.
Factory Girl is a testing helper library we use to simplify data creation in Rails. In it’s basic form, it makes creating test objects this simple:
1 2 3 4 5
1 2 3 4 5
Neat, right? Over time, your factories start to get some more meat on their bones:
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
So what’s the problem with the above setup (Other than that we are providing shotguns to an army of zombies)?
Well let’s see. Imagine (like I did) that you want to write a test for a worker that iterates over all of our users in a slow method:
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9 10 11
Imagine my surprise when the simple test took over 3 seconds to run! (And yes, I could stub out the sleep time to be shorter, or stub
User.all, but surely something else must be wrong?).
Diving into the code with pry, I realized that my simple creation of Thomas had created 2 lurking zombies.
See, what happens when I run the line
create(:user, ..., name: "Thomas Rogan") is factorygirl creates
gun objects (along with a default zombie user for each one), and after creating them sets their user properties to be the “Thomas” user.
The ‘zombie’ users that were originally created for those objects then just lurk around in the background and cause problems. The
User.all call was now arming these zombies as well as the Thomas. And taking over 3 seconds in the process.
What’s the big deal?
Ummm. Speed for starters – Every extra
has_one/has_many trait you add to those users spawns another zombie. That takes time. And not only time – it leads to bugs in your test code since you have extra objects you didn’t intend to create lying around.
Our code (we don’t actually deal with zombies, we set up people on 3-3 blind dates) had over half a dozen traits that were creating zombies on the user model alone. Which translates into serious time loss in our test suite, and bugs like the above.
Deriving a Solution
So how to solve this? Well the first thing I thought of was to move all of those traits that sneakily create another zombie to be
after_create callbacks so you can pass the user in to them:
1 2 3
Rewriting this simply for our main
User class resulted in a 10% speed increase in our test suite.
Unfortunately there’s a caveat. Not all test objects are created using factory girl’s
create strategy. Ideally, as much code as possible should be created using
build, or even better,
build_stubbed, which doesn’t persist to the database and is much faster. However, using this strategy to create a test user breaks because the
after_create hook is not called.
Given that ideally we want to be moving towards a situation where we use
build_stubbed more than
create, this seemed not ideal.
Should we write an
after(:build_stubbed) callback too then? Seems kind of wet. Not to mention, it won’t work because calling the
create factorygirl method runs both the
after_build callbacks, so our code would be run twice.
The alternative would be to define an extra set of traits for stubbed calls:
and then use these when building a stubbed model:
Zombie objects in stubbed calls don’t matter so much because they aren’t persisted and so don’t take much time to create and also aren’t returned in database queries so they won’t interfere with your other tests.
After a bit of time thinking about this, I decided we faced 2 alternatives.
- Just deal with the fact that we create extra objects – simple but slower
- Use the above method of
after_createhooks and separate trait names for stubbed/build calls. – less dry but faster
Each team prioritizes speed/code quality differently – so you may opt to deal with the extra users and test time to reduce code complexity.
As for us? We ended up going with option 2). A slight decrease in dryness with 10% gains in speed seem worth it to our company, at least at the moment.
- The above problem is only a problem for ‘has_one’/‘has_many’ relationships – by nature, those objects need to be created after the primary object has been created to be valid. So in the above code, a
factioncan be created before the user object has been created, so no sneaky extra objects are created.
- Thanks for reading – let me know if you have thoughts/comments.