RSpec issues with include
We're extracting modules from intellisource and that's a good engineering practice. We can localize and document the footprint of modules by requiring and including them where they're used.
[helper-module.gem] module HelperModule class HelperClass end end [intellisource] require 'helper_module' class X include HelperModule def m HelperClass.new end end
The problem is that RSpec does not work this way:
# rspec a_spec.rb # # 1) A should have HelperClass defined # Failure/Error: HelperClass.new # NameError: # uninitialized constant HelperClass # # ./a_spec.rb:10 # describe "A" do include HelperModule it "should have HelperClass defined" do HelperClass.new end end
As the rspec author comments on this issue:
I think the real problem we overloaded a Ruby keyword (include) to give the feeling of doing something that Ruby does, but in fact doesn't work the way it would therefore be expected.
include is not
include in RSpec (although it does work for
instance methods, so if you define a method in HelperModule, that will be
available). We're left few good options given what we found
This will result in a warning because you overwrite a shared constant:
# rspec b_spec.rb # # b_spec.rb:15: warning: already initialized constant X # (passes) describe "B1" do X = HelperModule::X it "should have X == HelperModule::X" do X.should == HelperModule::X end end describe "B2" do X = HelperModule::X it "should have X == HelperModule::X" do X.should == HelperModule::X end end
This suppresses the warning but you'll share the constant:
# rspec c_spec.rb # # 1) C1 should have X == HelperA::X # Failure/Error: X.should == HelperA::X # expected: HelperA::X # got: HelperB::X (using ==) # Diff: # @@ -1,2 +1,2 @@ # -HelperA::X # +HelperB::X # # ./c_spec.rb:15 # describe "C1" do X = HelperA::X it "should have X == HelperA::X" do X.should == HelperA::X end end describe "C2" do X = HelperB::X it "should have X == HelperB::X" do X.should == HelperB::X end end
The only surefire way you can do this is to use a fully-qualified constant.
# rspec d_spec.rb # (passes) describe "D1" do it "should have X == HelperA::X" do HelperA::X.should == HelperA::X end end describe "D2" do it "should have X == HelperB::X" do HelperB::X.should == HelperB::X end end
Alternatively you can use constants through the actual code that includes it (see e_spec, which passes).
RSpec significantly breaks expectations of how ruby works. This is bad and encourages us to adopt bad practices to compensate. For instance, redefining constants in the global namespace is one way to get around these issue.
B = A::B C = A::C
I beg that we don't do this. It's more lines of ugly to fully-qualify constants in RSpec but I argue it's a better practice overall because the issue is RSpec, not Ruby or namespaces in general.
Name collisions are a big deal and become more of an issue in a big app like ours. I think this indicates we should, as possible, use a truly class-based testing framework like Test::Unit where everything is predictable.