Testing with Rails’ “belongs_to” Without Associations
Belongs_to requires an association by default and that’s good, but painful for testing. Here’s how to write great tests regardless
Back in Rails 5, the venerable `belongs_to` attribute got an important update: it started to require an association by default.
As Rails is telling us, when we ran create!
, it looked up our user by the ID (500) in the database. It found out it didn’t exist and prevented the creation from going through by raising an error. While this isn’t as strict a guard as a database-level foreign key constraint, it gives us reasonable protection from creating orphaned rows in our database.
I’m a big fan of this change. Doing things wrong should be hard. However, this belongs_to
behavior applies in all environments including the test environment. For those of us who create database records in tests, that means we have to create more data. You know what the biggest culprit of slow test suites is? Creating data!
Creating data hurts most when our database lives on another server. This, of course, is the most common continuous integration test setup for production-grade applications. Every time we create a row, Rails fires off a round-trip to the database. Trips commonly take about 20–40ms. That’s not terrible, but it certainly adds up when the same test needs to create several rows and you have hundreds of tests. The worst inconvenience is when you have a belongs_to
hierarchy:
- A
BillingAddress
belongs to aUser
- A
User
belongs to aUserGroup
- A
UserGroup
belongs to aCompany
Now when we go to write the most straightforward test for a BillingAddress
controller it looks like this:
Holy smokes! Our two line test requires four lines of setup and four database round-trips! Since the database roundtrips will dominate our test time when run in a continuous integration environment, it’s not an exaggeration to say that our belongs_to
validation is quadrupling our test time.
Readers who have been employed by a large company using Java will know one way out of this: with dedicated effort, you can peel your database queries (the persistence layer) away from your object instantiation layer and then stub your persistence calls.
For the Rails crowd it’s even easier: you can just monkey-patch the find
, find_by
, or where
method used in the controller with a stub to return some canned data. If you’re using FactoryBot, that data would be provided by build
(which uses new
internally) instead of create
and thus avoid the database altogether. Skipping create
means that Rails won’t run the belongs_to
association existence check. It would look something like this:
Voila, no database access at all! It’s fast and not so bad to write. This is the right way (TM).
However, there are a variety of situations we don’t bring up in polite discussion but do run into. For example, this is an InTeGrAtIoN test, dammit! OR, your manager doesn’t believe in stubs because they besmirch the natural order of things (and because they came from a Java background). OR, your ex-co-worker wrote all these terrible tests and you just need to make them better, not perfect. For you, my discerning programmer, I’ve got a solution.
WAIT NO, don’t do that! While this example may (absolutely) be grounded in a thing we actually did (still do) at a company I worked (work) for, it is not the way! (Besides I swear, we’re reforming!)
This will solve your problem; you’ll be able to create
a billing address in your test but the cost is that you’ve lost the protection provided by the association-check validation in production! What we really want is a way to allow the validation to not run in our test environment. And it’d sure be swell if we didn’t have to define it as a parameter on every belongs_to
in our app. Luckily, Rails provides:
With that one line in your test environment config, every belongs_to
will skip it’s validation in the test environment only, allowing you to write concise, zingy tests like this:
While I’m not going to make the case that this is necessarily what you should do, the fact is, it’ll get you really, really far. It’s maintenance-free, easy-to-use, and requires only the one database access. And, it still maintains the belongs_to
validation that you want in prod (and even dev) environments.
There is a counter-argument that your test environment should behave as much like the production environment as possible. That’s valid. Mirroring your development and production config will alert you (by failing) when you write code that should create an association but doesn’t. However, because you’re a responsible programmer and because it’s important that your code creates that association, you ought to be testing that it does. If you don’t like risks, use stubs.
But for the rest of us who savor a little wild ride off the Rails, you’re welcome.
Resources
- Gist of a Rails relation resulting in a validation error
- Gist example of a test creating data for a set of hierarchical belongs_to relationships
- Gist of the same test with stubbed data retrieval
- Gist of the same test with just the relevant database row created
- Counterexample making just one belongs_to relationship optional in all environments
- Gist configuring the test environment to bypass the belongs_to validation