Spork and the Supermodel


Today I had (again) trouble with spork, the rails pre-loader. Here’s how we could finally beat it and may prevent others from falling into the same trap.

The problem

After being off of the project for some time I realized, that suddenly one of the rspec tests was failing. The mysterious test output was something like

×
expected <Stories::StatusPost...> to be kind of Stories::Base

We added those tests because we use a factory, which is set up in a way the rails sources also do it, i.e. we have a base.rb under stories and all subtypes defined here as well. And thus all Stories::* inherit from Stories::Base.

Additionally, when running all the specs via rake, everything shows up green, so only when run via guard this particular test was failing.

Digging into the problem, why this is suddenly not the case anymore, I printed out .superclass of the object in question, expecting it to be something else, but it wasn’t. Printing story.class.superclass == Stories::Base revealed a false and the object_ids of the classes themselves were different as well. So the question was: what’s going on here?

First thing was to find out the commit that introduced the bug. Luckily, because we had a test passing and failing immediately running guard, with the help of git bisect the “bad” commit could be found very fast – but wait: all it does is adding a new subclass exactly the way we had several existing already. WTF?

Ok - it’s only failing when using guard. Guard is set up to use spork to gain speed and the note from my colleague Björn who raised the question about some spork preloader code lead me to the old problem we had at the very beginning of our project, which we thought we had solved already. Here’s an excerpt of our spec_helper.rb

`ruby title=spec_helper.rb Spork.each_run do # reload all the models Dir["#{Rails.root}/app/models/**/*.rb"].each do |model| load model end # reload factories FactoryGirl.reload end

While this code helped us out of the trouble once, it unfortunately didn’t this time. Checking the responsible commit again and taking a look at the sidebar of files I realized that there actually is a difference between this (straight forward) addition and the former ones: The new class added was named ad.rb and was the only one in the file system above our base class named base.rb.

Conclusion

If you set up a class hierarchy the rails way, i.e. having your base class parallel to your subclasses, be aware of class loading order, because what the above spork code actually does is to (re)load all models one by one in alphabetical order!

First it finds ad.rb and loads it.

×
module Stories
  class Ad < Base
  end
end

Base is already loaded, so the base class of Ad is set.

Next it finds base.rb and reloads it – thus leading to a new Base class in memory while Ad‘s base class is pointing to the old one – letting the test fail.

One way to prove this was to add an explicit require_relative statement to the Ad class, but this was more than hacky and felt awkward. To avoid having to do this to all classes we might add in the future, a solution for the spork preloader was needed.

First I tried adding

×
ActiveSupport::Dependencies.clear

but this didn’t change anything. So we came up with

Spork.each_run do
  # reload all the models, base classes first to avoid having different base
  # classes loaded.
  Dir["#{Rails.root}/app/models/**/base.rb"].each do |model|
    load model
  end
  Dir["#{Rails.root}/app/models/**/*.rb"].each do |model|
    load model
  end
  # reload factories
  FactoryGirl.reload
end

While this does the trick, it still feels a bit strange, because

  • we have two Dir blocks searching the file system
  • base classes are loaded twice

but our tests are passing even when using guard with spork and that was the main issue.

If anyone has a better solution for this, please let me know.


Es gibt noch keine Kommentare