Rails 6 and Rspec : How to test Zeitwerk mode
By: Matthieu Varagnat
Tags: Testing, Rails 6, Continuous Integration

What is Zeitwerk mode?

Rails 6 introduced a new way of loading your application code, named Zeitwerk. It is the part of Rails that allows, for example, a model named User , defined in app/models/user.rb , to be used in other parts of the code without needing require User on top of every other file.

# app/models/user.rb

class User < ApplicationRecord

end

end # some other file

# no need for require User, it's already autoloaded

...

user = User.find(id)

...

With Zeitwerk, the way constants (Class and Modules) are autoloaded is somewhat optimized, or at least some of the pitfalls from the past are alleviated. The official documentation is quite detailed, and this article explains well the difference with the “classic” approach of previous Rails version.

The only important point to remember for this article is that Zeitwerk scans files at startup, and expect constants based on filenames : app/models/user.rb MUST define User .

Classic mode does the opposite : it lazyloads User , and when needed it looks for its code in app/models/user.rb .

Small difference, big consequences.

The Problem: how to test it

If you are managing an application in production, that a business relies upon, you probably have a solid test suite in place, hopefully with a good Continuous Integration system that allows you to push code to production without fear of breaking something. If you dont, you should — I honestly dont know how you can keep your sanity. At Wemind, thousands of members rely on us for insurance quotes and healthcare reimbursements, partners expect from us accurate data, and our internal tools must be completely reliable, so you can bet we have many, many tests covering almost our entire codebase.

And the first time we upgraded to Zeitwerk, the entire Wemind infrastructure went down crashing.

Unable to load application: Zeitwerk::NameError: expected file /app/app/jobs/obsolete_job.rb to define constant ObsoleteJob, but didn’t

The error was easy enough to spot and correct. The code in app/jobs/obsolete_job.rb had been commented out. Whereas Classic mode didnt bother since ObsoleteJob was never called, Zeitwerk scanned the file and expected it to define the constant.

The whole thing lasted only 15 minutes, but we are not used to downtime at Wemind. Why had it not been caught by tests ?

Turns out that, Test environment does NOT preload aggressively all the code, to speed things up. Therefore, it does NOT encounter the bug, which can then be pushed to Production and crash everything.

# config/environments/test.rb ...

# Do not eager load code on boot. This avoids loading your whole application

# just for the purpose of running a single test. If you are using a tool that

# preloads Rails for running tests, you may have to set it to true. config.eager_load = false

...

The solution

Solution 1

You can set config.eager_load = true . However, this will slow things down (I have not tested by how much. Perhaps it’s negligible?)

Solution 2

You can test Zeitwerk autoloading specifically by forcing one autoload. The test is super short to write and will catch production-breaking mistakes :

# not sure where to put it... spec/support/zeitwerk_spec.rb perhaps? require 'rails_helper' describe 'Zeitwerk' do

it 'eager loads all files' do

expect{ Zeitwerk::Loader.eager_load_all }.not_to raise_error

end

end

That’s it! Problem will not come back.