If you’ve ever encountered a situation where your Rails model associations work perfectly in the console but mysteriously fail in your test suite, you’re not alone. I recently ran into this exact issue and wanted to share the solution along with some insights about Rails’ autoloading behavior.
The Problem
I was working with a Rails app that uses concerns to share common functionality across models. One of these concerns, Addressable
, defines a polymorphic association:
module Addressable
extend ActiveSupport::Concern
included do
has_many :addresses, as: :addressable, dependent: :destroy
accepts_nested_attributes_for :addresses, allow_destroy: true, reject_if: :all_blank
end
def full_address
address = addresses.last
return "No address available" if address.nil?
# ... rest of method
end
end
My Estate
model includes this concern:
class Estate < ApplicationRecord
include BelongsToOrganisation
include Addressable
include ActiveAccountingPeriod
has_many :blocks
has_many :properties, through: :blocks
# ... other associations
end
Everything worked beautifully in the Rails console. I could create estates, add addresses, and call all the methods from the concern without any issues.
But then my FactoryBot tests started failing:
# spec/factories/estates.rb
FactoryBot.define do
factory :estate do
sequence(:name) { |n| "Estate #{n}" }
association :organisation
after(:create) do |estate|
estate.addresses << build(:address, addressable: estate)
end
end
end
The error was confusing:
NoMethodError: undefined method 'addresses' for an instance of Estate
The Investigation
What made this particularly puzzling was that the concern seemed to be loaded properly. When I added debug output to check the model’s ancestors, I could see Addressable
was included:
puts estate.class.ancestors
# => [Estate, ActiveAccountingPeriod, Addressable, BelongsToOrganisation, ...]
But when I checked the available associations:
puts estate.class.reflect_on_all_associations.map(&:name)
# => [:pay_customers, :charges, :subscriptions, :blocks, :properties, :expense_categories, :transactions, :accounting_periods]
Notice what’s missing? The addresses
association from the Addressable
concern wasn’t there.
The Root Cause
The issue stems from Rails’ different autoloading behavior between development and test environments:
Development Environment:
- Rails uses lazy loading
- When you reference
Estate
in the console, Rails loads the model and properly includes all concerns - The
included
block in concerns executes when the model is first loaded
Test Environment:
- Rails optimizes for fast test startup
- Models and concerns aren’t always loaded in the same order or manner
- The
included
block in concerns might not execute before your tests run
The Solution
The fix is to explicitly require your concerns and models in your test setup. Add this to your rails_helper.rb
:
# spec/rails_helper.rb
Dir[Rails.root.join('app/models/concerns/*.rb')].each { |f| require f }
Dir[Rails.root.join('app/models/*.rb')].each { |f| require f }
This ensures that all your models and their concerns are loaded before any tests run, making the test environment behave consistently with development.
Alternative Approaches
There are a few other ways to solve this issue:
1. Eager Load Everything
# In rails_helper.rb
RSpec.configure do |config|
config.before(:suite) do
Rails.application.eager_load!
end
end
This loads your entire application, which is more comprehensive but potentially slower.
2. Adjust Test Environment Configuration
# config/environments/test.rb
config.eager_load = true
Though this changes the fundamental behavior of your test environment.
3. Force Model Loading in Factories
# At the top of your factory file
Estate # Forces the model to load
FactoryBot.define do
factory :estate do
# ... factory definition
end
end
Why This Matters
This issue highlights an important aspect of Rails development: the framework’s autoloading behavior can vary between environments. While this usually works seamlessly, it can occasionally lead to subtle bugs that are hard to track down.
The explicit require
approach I used is targeted and fast—it only loads what you need rather than the entire application. It’s also explicit about the dependency, making it clear that your tests rely on these files being loaded.
Takeaways
- Environment differences matter: Always test your code in the same environment where it will run in production
- Explicit is better than implicit: When in doubt, explicitly require the files you need
- Debug systematically: Use Rails’ reflection methods to understand what’s actually loaded
- Consider autoloading: Be aware of how Rails loads your code in different environments
Have you encountered similar issues with concerns and testing? I’d love to hear about your experiences and solutions in the comments below.
This post originally started as a question to the Rails community on Discord and Twitter. Thanks to everyone who shared their insights and experiences!