Rails Testing Gotcha: When Concerns Work in Console But Not in Tests
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: ```ruby 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: ```ruby 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: ```ruby # 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: ```ruby puts estate.class.ancestors # => [Estate, ActiveAccountingPeriod, Addressable, BelongsToOrganisation, ...] ``` But when I checked the available associations: ```ruby 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`: ```ruby # 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 ```ruby # 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 ```ruby # config/environments/test.rb config.eager_load = true ``` Though this changes the fundamental behavior of your test environment. ### 3. Force Model Loading in Factories ```ruby # 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!