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.
Apparently 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
before.
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.