Controlling database usage in RSpec and factory_girl, part 1: Choosing and allowing appropriate strategies
When testing each layer of a Rails application, it’s important to create test model instances in the right way for that layer. Some specs need model instances that have been saved in the database, or that appear to have been; others need instances that have not. It’s also important to not use the database when you don’t have to, because every use makes your tests take a little bit longer.
This post, part 1 in a two-part series, lays out the appropriate use of factory_girl in specs of each layer of Rails, and gives a way to enforce that usage.
The type of model instance that an example needs depends partly on the layer of Rails that it tests:
- Model specs usually need model instances that have been saved in the database. For example, to test a query method, you need to save something for the query to find. Sometimes an instance that hasn’t been saved is acceptable or even necessary, such as when testing attribute validation — an invalid object can’t be saved in the database.
- Controller specs are best written to not use the database at all, with model methods stubbed to return whatever each example requires. This frees controller specs from worrying about the correctness of the model methods (which are already tested in model specs), and is also faster. However, model instances in controller specs need to look like they’re saved, because application code that uses them expects IDs. For example, if you call a path helper with an unsaved object, you’ll get an error like
ActionController::UrlGenerationError: No route matches {:controller=>"users", :action=>"show", :id=>nil}
. - Helper and view specs, like controller specs, should also not use the database, but need their test instances to look like they’ve been saved.
- Routing specs don’t need model instances at all!
- lib specs (that is, specs of code in the lib directory) likewise don’t need model instances, since lib is for low-level code that doesn’t depend on the rest of the app.
- feature specs (and Cucumber features) always need saved model instances. They’re integration tests, so they must use the database, speed be damned.
- And if you’ve defined your own layers, you’ll probably find that each of those layers has its own requirements for test model instances too.
factory_girl, the most popular model object factory gem, provides several methods for creating a model instance:
build
creates an instance that hasn’t been saved in the database, just as if it had been created withModelClass.new
. You can save it yourself later if you want.create
creates an instance in the database, just as if it had been created withModelClass.create!
.build_stubbed
creates an instance that has not been saved in the database, but that does have an ID, so appears to have been saved. Unlike objects created withbuild
, an object created withbuild_stubbed
can’t be saved — if you call a method likesave!
on a stubbed object, it will raise an error.
So each factory_girl method is only appropriate in specs of some layers of Rails:
- Model specs usually use
create
, and sometimes usebuild
(and those methods’_list
versions), neverbuild_stubbed
. - Controller, helper and view specs use
build_stubbed
(andbuild_stubbed_list
), neverbuild
orcreate
.build_stubbed
instances have the IDs that these specs expect, which is much more convenient than setting them manually.build_stubbed
instances also help prevent database usage in these specs, by raising an error when you forget to stub a method that would save the instance in the database. (It’s still up to you to stub other database-using methods on your model instances.) - Routing and lib specs don’t need factory_girl at all.
- feature specs always use
create
.
To prevent anyone from accidentally using a method in the wrong layer, instead of just doing
config.include FactoryGirl::Syntax::Methods
in your spec_helper.rb, include only the methods you need in each layer. Put something like this in spec/support/factories.rb
module MyApp module Factories module Model delegate :build, :build_list, :create, :create_list, to: FactoryGirl end module ControllerOrHelperOrView delegate :build_stubbed, to: FactoryGirl end module Feature delegate :create, :create_list, to: FactoryGirl end end end
If you use attributes_for
, decide what layers it’s appropriate in and put it there too.
In your spec_helper.rb, include each of those modules only in specs that need it:
config.include MyApp::Factories::Model, type: :model %i(controller helper view).each do |type| config.include MyApp::Factories::ControllerOrHelperOrView, type: type end
Now you’ll be unable to accidentally use the wrong instance type in an example, and unnecessary, slow database usage will be prevented.
Sounds great! Let’s run our controller and helper and view specs and admire our query-free test log! But what’s this? SELECTs everywhere? How can that be?
For the startling answer, tune in to Part 2: association strategy and unstubbed queries, floating ashore soon in a bottle near you.
Leave a Reply