Skip to content

Instantly share code, notes, and snippets.

@thinkerbot
Last active August 29, 2015 14:13
Show Gist options
  • Save thinkerbot/92ae28d1f18fb566847c to your computer and use it in GitHub Desktop.
Save thinkerbot/92ae28d1f18fb566847c to your computer and use it in GitHub Desktop.

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.

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).

Commentary

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.

module HelperModule
class HelperClass
end
end
describe "A" do
include HelperModule
it "should have HelperClass defined" do
HelperClass.new
end
end
module HelperModule
class X
end
end
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
module HelperA
class X
end
end
module HelperB
class X
end
end
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
module HelperA
class X
end
end
module HelperB
class X
end
end
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
module HelperA
class X
end
end
module HelperB
class X
end
end
class A
include HelperA
end
class B
include HelperB
end
describe "E1" do
it "should have X == HelperA::X" do
A::X.should == HelperA::X
end
end
describe "E2" do
it "should have X == HelperB::X" do
B::X.should == HelperB::X
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment