Better Programming

Advice for programmers.

Testing with Rails’ “belongs_to” Without Associations

Nick Francisci
Better Programming
Published in
4 min readJan 19, 2022

--

Photo by SxySelia on Unsplash

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:

  1. A BillingAddress belongs to a User
  2. A User belongs to a UserGroup
  3. A UserGroup belongs to a Company

Now when we go to write the most straightforward test for a BillingAddress controller it looks like this:

Assumes the use of RSpec and FactoryBot

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.

--

--

Nick Francisci
Nick Francisci

Written by Nick Francisci

Engineer motivated by helping folks access the fundamentals of life: wholesome food, education, and healthcare. I also make video games. Web: nickfrancisci.com

No responses yet