spring
spring copied to clipboard
rails + rspec + spring + eager_load = uninitialized constant
When I set eager_load = true in config/environments/test.rb then after spring is loaded and any file is changed then running specs again will fail with a message like "uninitialized constant User (NameError)".
I created simple rails project repository to reproduce it: https://github.com/radarek/rails_rspec_spring_eager_load_bug. It's pure rails project. I added only gems rspec-rails and spring-commands-rspec and generated User model. When I run bin/rspec specs pass, running it again still gives success. But after modifying app/models/user.rb file and running it again it will fail with a message user_spec.rb:3:in <top (required)>': uninitialized constant User (NameError)`.
I know that doesn't have to be a problem with spring itself but it's hard to guess what it causes.
Steps to reproduce:
$ git clone [email protected]:radarek/rails_rspec_spring_eager_load_bug.git
$ cd rails_rspec_spring_eager_load_bug
$ bin/rails db:migrate RAILS_ENV=test
$ bin/rspec
Running via Spring preloader in process 81040
.
Finished in 0.00345 seconds (files took 0.38525 seconds to load)
1 example, 0 failures
$ bin/rspec
Running via Spring preloader in process 81140
.
Finished in 0.00326 seconds (files took 0.3545 seconds to load)
1 example, 0 failures
$ echo "# comment" >> app/models/user.rb
$ bin/rspec
Running via Spring preloader in process 81305
/Users/radarek/programming/github/radarek/rails_rspec_spring_eager_load_bug/spec/models/user_spec.rb:3:in `<top (required)>': uninitialized constant User (NameError)
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in `load'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in `block in load_spec_files'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1433:in `each'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1433:in `load_spec_files'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:100:in `setup'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:86:in `run'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:71:in `run'
from /Users/radarek/.rbenv/gems/2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/runner.rb:45:in `invoke'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rspec-core-3.5.4/exe/rspec:4:in `<top (required)>'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `load'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/command_wrapper.rb:38:in `call'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:191:in `block in serve'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:161:in `fork'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:161:in `serve'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:131:in `block in run'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:125:in `loop'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application.rb:125:in `run'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application/boot.rb:19:in `<top (required)>'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/2.4.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from /Users/radarek/.rbenv/versions/2.4.0/lib/ruby/2.4.0/rubygems/core_ext/kernel_require.rb:55:in `require'
from -e:1:in `<main>'
I have almost same problem, but rspec throws error even on second run without modifying any files. I faced with this problem after enabling eager_loading for test environment
Apparently this is due to Rails 5 disabling autoloading (and thus auto-reloading) of constants when config.eager_load = true.
However, thanks to this article I learned that it's possible to re-enable that behaviour using config.enable_dependency_loading = true. From the Rails guides:
config.enable_dependency_loading: when true, enables autoloading, even if the application is eager loaded andconfig.cache_classesis set as true. Defaults to false.
With that line added to test.rb, your reproduction steps start working again. :) Thanks for creating the sample app!
@radarek @lime An alternative to enable_dependency_loading is to just turn eager_load off: https://github.com/timdiggins/rails_rspec_spring_eager_load_bug/tree/no-eager-loading.
It doesn't AFAICT gain anything to turn eager_load on (despite the very old (2012) message about turning it on when using a pre-loader (spring) in the generated config: https://github.com/rails/rails/blame/master/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt#L12 https://github.com/rails/rails/commit/e6747d87f3a061d153215715d56acbb0be20191f).
Reading the history of https://github.com/rails/spring/blame/master/lib/spring/application.rb has helped, but this is mostly very old (older than the changes to eager_load)
However I'm still working through this approach in practice. It may mean you need to explicitly spring stop more often. Alternatively it might mean you need to set cache_classes = false (and there might be a performance hit if so).
I find that config.enable_dependency_loading sometimes not work correctly. If autoloading has been working fine for you, then the most straighforward and run in predictable manner is diffing the eager_load_paths and auto_load_paths and add the diff to eager_load_paths. That's what I do in my project with external engines and unconventional code locations.
load_disparities = (ActiveSupport::Dependencies.autoload_paths - config.eager_load_paths).uniq
config.eager_load_paths += load_disparities unless Rails.env.development?
I prefer to keep autoload in test to true, to keep cfg between test and prod the same
I'm using minitest in a Rails engine and encountered a similar issue. None of the proposed solutions worked for me. I found that adding my autoload paths to ActiveSupport::Dependencies.autoload_paths did work for me:
ActiveSupport::Dependencies.autoload_paths += [
File.absolute_path(File.join(__dir__, 'support/'))
]