Every example group becomes a real Ruby class, and RSpec assigns it a constant under RSpec::ExampleGroups. A describe Project do ... end in spec/models/project_spec.rb defines RSpec::ExampleGroups::Project.
That constant is harmless for the Ruby code in your specs. But it can shadow your Project model when Rails resolves an association class by name — and it only does so when the two spec files happen to load into the same process. On a sharded CI you get a test that fails on some runs and not others.
How affected tests look like
Take a look at this spec that embeds a Thing class:
# spec/models/shadowing_spec.rb
describe Project do
end
describe 'some unrelated group' do
class self::Thing < ActiveRecord::Base
self.table_name = 'projects'
belongs_to :project
end
it 'resolves the association' do
self.class::Thing.new.project
end
end
Running this test will raise an error:
ArgumentError:
The Project model class for the RSpec::ExampleGroups::SomeUnrelatedGroup::Thing#project
association is not an ActiveRecord::Base subclass.
Delete the empty describe Project block and it passes.
Note
The two groups are siblings, not nested — and in real life they are usually in different files. >
RSpec::ExampleGroups::Projectis defined when thedescribe Projectblock is loaded, and it stays defined for the rest of the process. It is not scoped to the block, and it does not go away when the block ends or when its examples have run.
Fix
Anchor the class name:
belongs_to :project, class_name: '::Project'
compute_type short-circuits on a leading :: and skips the candidate walk entirely.
Alternatively, move the throwaway class out of the example group to the top level of the file, where it is no longer named under RSpec::ExampleGroups.
What is NOT affected
Normal constant references in specs are fine:
describe Project do
it 'works' do
puts Project # => Project (your model)
create(:project) # fine
Project.where(...) # fine
end
end
Ruby resolves those lexically. The block is written at the top level of the file, so Module.nesting is empty and Project is ::Project. A block does not open a namespace, and RSpec::ExampleGroups::Project is simply not reachable from there. Ruby's constant lookup never sees it.
Also unaffected:
-
Models and other classes defined at the top level of a spec file. Only classes defined inside an example group get a name under
RSpec::ExampleGroups::…, which is what feedscompute_type. -
Your application code. Nothing here changes how
app/models/**resolves constants.
So you need all three at once: a Rails-managed class defined inside an example group, a group whose description camelizes to a colliding model name, and both files loaded into the same process.
Why this happens
RSpec names every group (rspec-core, ExampleGroups.assign_const). The name comes from the description string, camelized — so describe 'Project' shadows just as well as describe Project.
Rails' compute_type then resolves belongs_to :project by building candidate constant paths from the name string of the owning class, from the inside out:
RSpec::ExampleGroups::SomeUnrelatedGroup::Thing::Project
RSpec::ExampleGroups::SomeUnrelatedGroup::Project
RSpec::ExampleGroups::Project # <- found here, search stops
RSpec::Project
Project # <- never reached
The bare Project is appended last, so any RSpec::ExampleGroups::…::Project wins. Rails then checks that what it found is an ActiveRecord::Base subclass, and raises.
Both groups hang off RSpec::ExampleGroups, and the walk passes through it on the way out. That is why a sibling group is enough: any top-level group named Project anywhere in the loaded suite shadows the model for every class defined inside any example group.