rspec / rspec-mocks

RSpec's 'test double' framework, with support for stubbing and mocking
https://rspec.info
MIT License
1.16k stars 356 forks source link

Feature proposal: constant stubbing #144

Closed myronmarston closed 12 years ago

myronmarston commented 12 years ago

A few months ago, I added constant stubbing functionality to @xaviershay's rspec-fire. From the start, I thought this would be a great addition to rspec-mocks, but wanted to vet it and work out the kinks in rspec-fire first. At this point, i'd like to get feedback on adding it rspec-mocks. I know that @garybernhardt is eager to see it added here.

Here's how the feature works in rspec-fire (and how I'd imagine it working in rspec-mocks):

module A
  module B
  end

  module C
    module D
    end
  end

  module E
  end

  F = 5
end

stub_const("A", Module.new)
A # => the new module
# A::B, A::C, A::E, A::F => undefined const errors

stub_const("A", Module.new, :transfer_nested_constants => true)
A # => the new module
A::B # => the original A::B
A::C # => the original A::C
A::E # => the original A::E
A::F # => 5

stub_const("A", Module.new, :transfer_nested_constants => [:B, :F])
# => only A::B and A::F are transferred

# This will raise an error; since A::F isn't a module (and hence can't have constants nested under it)
# there are no constants to transfer.
stub_const("A::F", Module.new, :transfer_nested_constants => true)

# This will raise an error; constants can only be transferred to a module, but our stubbed value is not a module
stub_const("A", Object.new, :transfer_nested_constants => true)

I've found this to be very useful for a couple reasons:

So...I'd love to get both feedback from the community and buy-in from other rspec contributors before I start working on this.

Thoughts?

/cc @dchelimsky @justinko @garybernhardt @xaviershay @freelancing-god @coreyhaines

dchelimsky commented 12 years ago

This all sounds good to me except:

There's a caveat to that last point: if, during the example, the value of the constant is changed by the user (e.g. because the example loads a file that redefines the constant, or whatever), then the constant is not restored to its original value; the thinking is that if the user is changing the constant on their own then they know what they're doing and we shouldn't change it out underneath them.

Personally, I'd rather be able to rely on trusting that no matter what happens in my examples, the state is restored at the end of each.

myronmarston commented 12 years ago

I don't feel very strongly either way on that point. At the time I was working on the code in rspec fire, it made sense to me to do it that way, but in retrospect, I'm not sure it was the right choice.

Also, I just tried this:


class Foo
  def self.bar
    "original bar"
  end
end

describe Foo do
  specify do
    Foo.stub(:bar) { "stubbed bar" }
    def Foo.bar; "overridden bar"; end
  end

  specify do
    Foo.bar.should eq("original bar")
  end
end

...and I'm seeing that stubbed methods get restored even if they get re-defined by the user after being stubbed. I think rspec-mocks should be consistent here, and always restore the original state of constants no matter what.

garybernhardt commented 12 years ago

I'd rather have the cleanup happen unconditionally as well.

justinko commented 12 years ago

:+1:

This is just great. Would allow me to get rid of my home grown stub_const that does not "rollback"!

xaviershay commented 12 years ago

This would be awesome because it means less code for me to maintain in rspec-fire :P

(which actually isn't a problem because @myronmarston does such a good job of it...)