Skip to content

Loading…

Constant stubbing #146

Merged
merged 16 commits into from

5 participants

@myronmarston
RSpec member

This is the implementation of constant stubbing for #144.

I want to do the work in rspec-fire to ensure that it works with this before merging it in, but it's done as far as I know for now, so I figured I'd submit the pull request to get implementation feedback.

myronmarston added some commits
@myronmarston myronmarston First pass at implementing constant stubbing.
This is almost copied verbatim from rspec-fire.

For #144.
9c44b28
@myronmarston myronmarston Always restore original constants.
The original logic from rspec-fire did not restore original constants
if the user changed them in the example after stubbing them, but after
talking it over with @dchelimsky and @garyberhnardt we've decided to
be consistent and always restore them.

For #144.
9a7c90d
@myronmarston myronmarston Remove the bang from our #stub! methods.
I'm not really sure why I used them when I wrote this in rspec-fire;
given there were not corresponding bang-less methods, it didn't
really make sense.

For #144.
2b4fad7
@myronmarston myronmarston Fix a constant stubbing edge case.
stub_const("A::B::C", whatever) cannot work if A::B is defined
but A::B is not a module.

For #144.
d1669a8
@myronmarston myronmarston Remove unused method.
For #144.
f77357e
@myronmarston myronmarston Add API docs for new constant stubbing code.
For #144.
ecfb7f6
@myronmarston myronmarston Add cukes for new stub_const feature.
Closes #144.
e31f4a4
@justinko justinko commented on an outdated diff
README.md
@@ -246,6 +246,68 @@ While this is a good thing when you really need it, you probably don't really
need it! Take care to specify only the things that matter to the behavior of
your code.
+## Stubbing Constants
+
+Support is provided for stubbing constants. Like with method stubs, the
+stubbed constants will be restored to their original state when a method
+completes.
@justinko
justinko added a note

This should be "when an example completes"

@myronmarston RSpec member

Thanks for noticing, I just pushed a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@justinko justinko commented on the diff
README.md
@@ -246,6 +246,68 @@ While this is a good thing when you really need it, you probably don't really
need it! Take care to specify only the things that matter to the behavior of
your code.
+## Stubbing Constants
@justinko
justinko added a note

Maybe we should just add a link to features/stubbing_constants/README.md since they are identical.

@myronmarston RSpec member

We certainly could. We could make them different, too. I just copied and pasted to make it easy on myself :). @dchelimsky -- got a preference?

@justinko
justinko added a note

If you want to be clever, you could regex it from the README and add a file to the cukes in the "relish" rake task. We already do this for the Changelog.

@dchelimsky RSpec member

Putting a link in the README is fine for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@justinko justinko commented on the diff
spec/rspec/mocks/stub_const_spec.rb
((64 lines not shown))
+ end
+
+ shared_examples_for "unloaded constant stubbing" do |const_name|
+ before { recursive_const_defined?(const_name).should be_false }
+
+ define_method :const do
+ recursive_const_get(const_name)
+ end
+
+ define_method :parent_const do
+ recursive_const_get("Object::" + const_name.sub(/(::)?[^:]+\z/, ''))
+ end
+
+ define_method :last_const_part do
+ const_name.split('::').last
+ end
@justinko
justinko added a note

You can get rid of the duplication of these "helper" methods by two ways:

module ConstNameHelpers
  include RSpec::Mocks::RecursiveConstMethods

  def const
    recursive_const_get(self)
  end
end

shared_examples_for "unloaded constant stubbing" do |const_name|
  const_name.extend ConstNameHelpers

  it 'allows it to be stubbed' do
    const_name.const.should_not eq(7)
  end
end

OR

module ConstHelpers
  include RSpec::Mocks::RecursiveConstMethods

  def self.[](const_name)
    @@const_name = const_name
    self
  end

  def const
    recursive_const_get(@@const_name)
  end
end

shared_examples_for "unloaded constant stubbing" do |const_name|
  include ConstHelpers[const_name]

  it 'allows it to be stubbed' do
    const.should_not eq(7)
  end
end
@justinko
justinko added a note

You could also nest an include_examples (although you'd have to make it support a block, or just use shared_examples):

shared_examples_for "unloaded constant stubbing" do |const_name|
  include_examples 'const helpers', const_name

  it 'allows it to be stubbed' do
    const.should_not eq(7)
  end
end

This might be considered "abuse" though....

@myronmarston RSpec member

I think all of these reduce the clarity of the code significantly, so I'm going to stick with what I have, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@justinko justinko commented on the diff
lib/rspec/mocks/stub_const.rb
((142 lines not shown))
+ remaining_parts.shift
+ klass.const_get(name)
+ end
+
+ context = remaining_parts.inject(@deepest_defined_const) do |klass, name|
+ klass.const_set(name, Module.new)
+ end
+
+ @const_to_remove = remaining_parts.first || const_name
+ context.const_set(const_name, @stubbed_value)
+ end
+
+ def rspec_reset
+ @deepest_defined_const.send(:remove_const, @const_to_remove)
+ end
+ end
@justinko
justinko added a note

Looks like UndefinedConstantSetter and DefinedConstantReplacer share some functionality. Here is a parent class they could share:

class StubbableConstant
  attr_reader :original_value, :full_constant_name

  def initialize(full_constant_name, stubbed_value)
    @full_constant_name = full_constant_name
    @stubbed_value = stubbed_value
  end

  # methods that #stub can use, which would "thin" it out a bit
  private

  def context_parts
    @full_constant_name.split('::')
  end

  def const_name
    @const_name ||= context_parts.pop
  end
end

Which would allow:

class DefinedConstantReplacer < StubbableConstant
  def initialize(full_constant_name, stubbed_value, transfer_nested_constants)
    super(full_constant_name, stubbed_value)
    @transfer_nested_constants = transfer_nested_constants
  end
end
@myronmarston RSpec member

Thanks, I just pushed a slightly different version of what you suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
myronmarston added some commits
@myronmarston myronmarston Replace duplicated README content with a link.
As per the conversation with @justinko and @dchelimsky:
#146 (comment)

Note that this link is broken for now because this hasn't
yet been merged into master. But I figured it was better
not to use a working link just to the branch since that
branch will likely be deleted in the near future.
0bd5850
@myronmarston myronmarston Refactor constant stubbers a bit.
This is based on @justinko's suggestions:
#146 (comment)
2cbf9c4
@myronmarston myronmarston Remove unused helper method. 22c2049
@myronmarston myronmarston commented on an outdated diff
lib/rspec/mocks/example_methods.rb
@@ -41,6 +41,47 @@ def allow_message_expectations_on_nil
Proxy.allow_message_expectations_on_nil
end
+ # Stubs the named constant with the given value.
+ # Like method stubs, the constant will be restored
+ # to its original value (or lack of one, if it was
+ # undefined) when the example completes.
+ #
+ # @param constant_name [String] The fully qualified name of the constant. The current
+ # constant scoping at the point of call is not considered.
+ # @param value [Object] The value to make the constant refer to. When the
+ # example completes, the constant will be restored to its prior state.
+ # @param options [Hash] Stubbing options.
+ # @option options :transfer_nested_constants [Boolean, Array<Symbol>] Determines
+ # what nested constants, if any, will be transferred from the original value
+ # of the constant to the new value of the constant. This only works if both
+ # the original and new values are modules (or classes).
+ # @return [Object] the original value of the constant if it was already defined
@myronmarston RSpec member

@dchelimsky / @justinko / @alindeman -- I want to get your feedback on this. stub_const returns the original value of the constant (if it was already defined). This is used by rspec-fire and just seems generally useful.

That said, I'm wondering about the interface for this. As it currently stands, it's impossible to tell (based on the return value) the difference between an undefined constant and a defined constant that was set to nil--either way, nil will be returned. I'm considering changing this so that rather than returning the original constant value, it would yield the the original constant value, and it would only yield if the constant was already defined. If the constant was not defined, it would not yield at all...so you could tell the difference. stub_const would no longer have a meaningful return value.

I'm not sure which is better; the return value interface is straightforward, and how often are constants defined and set to nil? On the other hand, having an unambiguous interface is nice.

Thoughts?

@justinko
justinko added a note

We should be consistent: double returns a RSpec::Mocks::Mock, stub_const should return one of the two classes you created. You've already got the reader: original_value

@justinko
justinko added a note

Regarding my comment above: It's confusing to return two different types, when they are both "stub const". Maybe returning an instance of StubbedConstant (a new class) with original_value set for real constants, and nil for "fake" constants.

@dchelimsky RSpec member

@justinko I think stub_const is different enough that it doesn't need to be constrained by double's API. I'm OK with your suggestion (which doesn't align w/ double IMO) to return a StubbedConstant object with the original_value hanging off it. I also like @myronmarston's idea of yielding when the constant was previously defined. It aligns with ActiveRecord's find_or_create_by APIs. I'd like some other opinions @alindeman, @patmaddox, @spicycode.

@justinko
justinko added a note

I think stub_const is different enough that it doesn't need to be constrained by double's API. I'm OK with your suggestion (which doesn't align w/ double IMO)

Yeah, double was a bad example. should_receive returns a MessageExpectation. Just because stub_const doesn't have an explicit receiver doesn't mean it shouldn't be consistent with RSpec's other "mocking" methods.

@dchelimsky RSpec member

MessageExpectation is related to the thing we're building. The reason stub and should_receive return MessageExpectation is so we can chain the other declaration methods e.g. obj.should_receive(:message).with(stuff).and_return(other_stuff). End users don't assign the MessageExpectation to a variable.

stub_const is going to either return or yield something related to thing we're replacing, not building. It can't, by definition, be consistent with should_receive or stub.

@dchelimsky RSpec member

Just to confuse things further: even if having stub_const return or yield the replaced value is useful, it still feels awkward to me. At first glance, I'd expect a method named stub_const to return the stub (in which case @justinko's consistency argument would make more sense to me).

Also, I can't think of a good use case for this. When would you ever want a handle on the original value? Even if you could argue it, why not just leave it to the user to assign the real value to a variable?

real_thing = THING
stub_const("THING", fake_thing)
# etc

I imagine that the biggest use case of constant stubbing will be to replace Classes.

What if stub_const defaulted to replacing the constant with Class.new (value=Class.new) and returned the new stubbed constant? That would allow things like:

stub_const("MyModel").stub(:find_by_foo).and_return(bar)

Like @dchelimsky, I am having trouble finding a use case for doing something with the original constant.

@dchelimsky RSpec member

I think if it's going to return anything by default it should be a double. That supports @alindeman's example without adding methods like new, etc.

Yah, a double feels even better. +1

@myronmarston RSpec member

Also, I can't think of a good use case for this. When would you ever want a handle on the original value? Even if you could argue it, why not just leave it to the user to assign the real value to a variable?

This came out of rspec-fire, where we need to keep a reference to the old value. This is necessary because when you do fire_replaced_class_double("MyNamespace::MyClass") it gives you a test double that verifies each stub or mock expectation you set on it against the interface of MyNamespace::MyClass if the constant was already defined. That way, if you stub a method that doesn't exist, you get an error immediately. The reference to the old value is necessary to make this possible.

When I ported this from rspec-fire, I had to decide where to "split" the code there, and this is what made sense at the time. In my head, I thought that returning the original value would be generally useful, but I can't think of any specific use cases beyond what rspec-fire is doing right now. As for leaving it to the user/rspec-fire to assign the original constant value to a variable...that's certainly doable, although in the case of rspec-fire, it's not as simple as your real_thing = THING example...rspec-fire will have to recursively get the real constant since it just has the string representation. Not a huge deal, but it's work that rspec-mocks will also do, so it's a bit wasteful to do it multiple times (admittedly, the perf cost will be unnoticeable unless people are doing crazy stuff where they have a constant nested thousands of levels deep).

Anyhow, there was another thing we had in rspec-fire that I originally ported but then removed: find_original_value_for(constant_name). We still need that functionality in rspec-fire and I was planning to add it back there (since it didn't feel like it belonged here, originally), but if there's interest in keeping that here, it would solve the problem as well...then we could just let users use that to get original values if they want them and don't want to stash original values in a variable.

Whatever you guys want me to do here is fine; my perspective is very much colored by how we used this in rspec-fire, but I want to make this feature generally useful and not couple it to what the needs of rspec-fire are.

I think if it's going to return anything by default it should be a double.

double is probably the right thing to have it return if we're going to have a default. I lean a little towards not making a default for now, though, unless there's near-unanimous support for that. We can always add the default later, but if we add it now and later decide something else makes more sense, we'll be stuck with it.

/cc @xaviershay

@dchelimsky RSpec member

I agree that no default is the right first step.

re: getting a handle on the original, I appreciate that stub_const can provide the right thing, but it still feels awkward to me to get the original from that method. I'm curious what others might think. Anybody else listening to this besides @myronmarston, @alindeman and @justinko?

No default to start sounds good to me. +1 for returning something other than the original: the new constant seems most reasonable to me at this time.

@myronmarston RSpec member

The more I think about it, the more I think that @alindeman's idea to return the new constant value makes sense. That allows easy chaining (e.g. to stub the constant with a mock object, then stub things on the mock object).

So, I think there's just one remaining question: do we expose a public API that projects like rspec-fire can use to find original values for stubbed constants (such as ConstantStubber.find_original_value_for(constant_name))? Or is the responsibility of rspec-fire to implement that itself? I'll be implementing it either way (in one project or the other...), but it would definitely be easier to implement it in rspec-mocks, because the implementation can use private internal APIs if it's on rspec-mocks; if I put it in rspec-fire, I only want to rely on public APIs so it'll be a bit trickier to implement. Is it generally useful enough to include in rspec-mocks? Or will it just be bloat here?

@dchelimsky RSpec member

IMO there should be an API for it. I just don't know what it should be. I do like the idea of making it a module function (e.g. ConstantStubber.find_original_value_for(constant_name)) but maybe something less wordy like StubbedConstants.original name. Other ideas?

@myronmarston RSpec member

I just pushed a first pass at this:

211743d

Originally, I was going to have the method return the raw original value, but I realized there are a couple edge cases here:

  • What does nil mean? That the constant was originally set to nil? Or that the constant was previously undefined?
  • What should be returned for a constant that hasn't been stubbed?

I realized it's a lot easier to communicate each of the possibilities by returning an struct-like object.

I still need to document it, but I wanted feedback first. Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@myronmarston myronmarston Return stubbed value rather than original value from stub_const.
This allows chaining:

  stub_const("Foo", double).stub(:foo)
f94e6b2
@travisbot

This pull request passes (merged f94e6b2 into 4b70b45).

@myronmarston myronmarston Add `Constant.original` API to query rspec-mocks about stubbed consta…
…nts.

This needs to be documented, but I want to get feedback from others before spending effort on that.
211743d
@travisbot

This pull request passes (merged 211743d into 4b70b45).

@dchelimsky

I'm not sure I'm comfortable with the name Constant. Seems too generic to me - likely to conflict. WDYT?

RSpec member

I started with the name StubbedConstant, but it seemed funny to have a method StubbedConstant#stubbed? and it made me realize you can get one of these for a constant that has not been stubbed, so I shortened it to Constant.

"likely to conflict"--you mean within rspec-mocks or in end-user test suites? It's properly namespaced within RSpec::Mocks so I'm not too concerned about name conflicts. Also, I don't think end users will ever manually construct one of these.

Still, if you've got a better idea for a name, I'm all ears :).

@dchelimsky

Make unstubbed private?

RSpec member

Will do.

@dchelimsky

This is a beautiful example of how to properly use its! However, given that I've been talking about removing its from rspec-3, I'd like to not add more itss to rspec's own suite. How about:

describe Constant do
  describe ".original" do
    context 'for a previously defined unstubbed constant' do
      let(:orig) { Constant.original("TestClass::M") }

      it("exposes name")                         { orig.name.should eq "TestClass::M" }
      it("exposes original_value")               { orig.original_value.should eq :m }
      it("returns true for previously_defined?") { orig.previously_defined?.should be_true }
      it("returns false for stubbed?")           { orig.stubbed?.should be_false }
    end

    # ...
  end
end

Slightly noisier but quite readable. In fact, seeing orig.original_value this way makes me realize the redundancy in the name original_value, which I think could just be value: orig.value. And perhaps orig. defined? instead of orig.previously_defined???

Also, the output of --format documentation is cleaner this way.

RSpec member

This is a beautiful example of how to properly use its! However, given that I've been talking about removing its from rspec-3, I'd like to not add more itss to rspec's own suite. How about:

Haha, yeah, I almost didn't use its for that reason, but it felt nice to have a valid use case for it :). I like your refactoring, and you're right; the documentation output will be better.

In fact, seeing orig.original_value this way makes me realize the redundancy in the name original_value, which I think could just be value: orig.value. And perhaps orig. defined? instead of orig.previously_defined???

I think it looks redundant when you've chosen orig as your local variable name, or if you're chaining it directly off of Constant.original...but when a user does something like:

const = Constant.original("SomeConst")
# a bunch of code goes in between here
const.original_value
const.previously_defined?

It's nice to have the more descriptive names. In this context, const.value would sound (to me, at least) like it would return the current value; and const.defined? sounds like it returns whether or not the constant is currently defined. I think the longer names bring more clarity (it's really hard to misinterpret them!) even though it can be a bit redundant with a variable name like orig.

myronmarston added some commits
@myronmarston myronmarston Make Constant.unstubbed private since it's an internal API. fd595a8
@myronmarston myronmarston Refactor away the use of #its.
#its is going away in rspec-core at some future point, so we shouldn't use it here.
722529e
@travisbot

This pull request passes (merged 722529e into 4b70b45).

@travisbot

This pull request passes (merged 3b63c51 into 4b70b45).

@myronmarston myronmarston merged commit f5c63c3 into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 2, 2012
  1. @myronmarston

    First pass at implementing constant stubbing.

    myronmarston committed
    This is almost copied verbatim from rspec-fire.
    
    For #144.
  2. @myronmarston

    Always restore original constants.

    myronmarston committed
    The original logic from rspec-fire did not restore original constants
    if the user changed them in the example after stubbing them, but after
    talking it over with @dchelimsky and @garyberhnardt we've decided to
    be consistent and always restore them.
    
    For #144.
  3. @myronmarston

    Remove the bang from our #stub! methods.

    myronmarston committed
    I'm not really sure why I used them when I wrote this in rspec-fire;
    given there were not corresponding bang-less methods, it didn't
    really make sense.
    
    For #144.
  4. @myronmarston

    Fix a constant stubbing edge case.

    myronmarston committed
    stub_const("A::B::C", whatever) cannot work if A::B is defined
    but A::B is not a module.
    
    For #144.
  5. @myronmarston

    Remove unused method.

    myronmarston committed
    For #144.
  6. @myronmarston
  7. @myronmarston
Commits on Jun 3, 2012
  1. @myronmarston
  2. @myronmarston

    Replace duplicated README content with a link.

    myronmarston committed
    As per the conversation with @justinko and @dchelimsky:
    #146 (comment)
    
    Note that this link is broken for now because this hasn't
    yet been merged into master. But I figured it was better
    not to use a working link just to the branch since that
    branch will likely be deleted in the near future.
  3. @myronmarston

    Refactor constant stubbers a bit.

    myronmarston committed
    This is based on @justinko's suggestions:
    #146 (comment)
Commits on Jun 4, 2012
  1. @myronmarston
Commits on Jun 7, 2012
  1. @myronmarston

    Return stubbed value rather than original value from stub_const.

    myronmarston committed
    This allows chaining:
    
      stub_const("Foo", double).stub(:foo)
  2. @myronmarston

    Add `Constant.original` API to query rspec-mocks about stubbed consta…

    myronmarston committed
    …nts.
    
    This needs to be documented, but I want to get feedback from others before spending effort on that.
Commits on Jun 8, 2012
  1. @myronmarston
  2. @myronmarston

    Refactor away the use of #its.

    myronmarston committed
    #its is going away in rspec-core at some future point, so we shouldn't use it here.
Commits on Jun 11, 2012
  1. @myronmarston
View
6 README.md
@@ -246,6 +246,12 @@ While this is a good thing when you really need it, you probably don't really
need it! Take care to specify only the things that matter to the behavior of
your code.
+## Stubbing Constants
@justinko
justinko added a note

Maybe we should just add a link to features/stubbing_constants/README.md since they are identical.

@myronmarston RSpec member

We certainly could. We could make them different, too. I just copied and pasted to make it easy on myself :). @dchelimsky -- got a preference?

@justinko
justinko added a note

If you want to be clever, you could regex it from the README and add a file to the cukes in the "relish" rake task. We already do this for the Changelog.

@dchelimsky RSpec member

Putting a link in the README is fine for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+See the [stubbing constants
+README](https://github.com/rspec/rspec-mocks/blob/master/features/stubbing_constants/README.md)
+for info on this feature.
+
## Use `before(:each)`, not `before(:all)`
Stubs in `before(:all)` are not supported. The reason is that all stubs and mocks get cleared out after each example, so any stub that is set in `before(:all)` would work in the first example that happens to run in that group, but not for any others.
View
3 features/.nav
@@ -18,6 +18,9 @@
- explicit.feature
- general_matchers.feature
- type_matchers.feature
+- stubbing_constants:
+ - stub_defined_constant.feature
+ - stub_undefined_constant.feature
- outside_rspec:
- configuration.feature
- standalone.feature
View
62 features/stubbing_constants/README.md
@@ -0,0 +1,62 @@
+## Stubbing Constants
+
+Support is provided for stubbing constants. Like with method stubs, the
+stubbed constants will be restored to their original state when an
+example completes.
+
+``` ruby
+stub_const("Foo", fake_foo)
+Foo # => fake_foo
+```
+
+Stubbed constant names must be fully qualified; the current module
+nesting is not considered.
+
+``` ruby
+module MyGem
+ class SomeClass; end
+end
+
+module MyGem
+ describe "Something" do
+ let(:fake_class) { Class.new }
+
+ it "accidentally stubs the wrong constant" do
+ # this stubs ::SomeClass (in the top-level namespace),
+ # not MyGem::SomeClass like you probably mean.
+ stub_const("SomeClass", fake_class)
+ end
+
+ it "stubs the right constant" do
+ stub_const("MyGem::SomeClass", fake_class)
+ end
+ end
+end
+```
+
+When you stub a constant that is a module or a class, nested
+constants on the original module or class are not transferred
+by default, but you can use the `:transfer_nested_constants`
+option to tell rspec-mocks to transfer them:
+
+``` ruby
+class CardDeck
+ SUITS = [:Spades, :Diamonds, :Clubs, :Hearts]
+ NUM_CARDS = 52
+end
+
+fake_class = Class.new
+stub_const("CardDeck", fake_class)
+CardDeck # => fake_class
+CardDeck::SUITS # => raises uninitialized constant error
+CardDeck::NUM_CARDS # => raises uninitialized constant error
+
+stub_const("CardDeck", fake_class, :transfer_nested_constants => true)
+CardDeck::SUITS # => [:Spades, :Diamonds, :Clubs, :Hearts]
+CardDeck::NUM_CARDS # => 52
+
+stub_const("CardDeck", fake_class, :transfer_nested_constants => [:SUITS])
+CardDeck::SUITS # => [:Spades, :Diamonds, :Clubs, :Hearts]
+CardDeck::NUM_CARDS # => raises uninitialized constant error
+```
+
View
79 features/stubbing_constants/stub_defined_constant.feature
@@ -0,0 +1,79 @@
+Feature: Stub Defined Constant
+
+ Use `stub_const` to stub constants. When the constant is already defined,
+ the stubbed value will replace the original value for the duration of the
+ example.
+
+ Scenario: Stub top-level constant
+ Given a file named "stub_const_spec.rb" with:
+ """ruby
+ FOO = 7
+
+ describe "stubbing FOO" do
+ it "can stub FOO with a different value" do
+ stub_const("FOO", 5)
+ FOO.should eq(5)
+ end
+
+ it "restores the stubbed constant when the example completes" do
+ FOO.should eq(7)
+ end
+ end
+ """
+ When I run `rspec stub_const_spec.rb`
+ Then the examples should all pass
+
+ Scenario: Stub nested constant
+ Given a file named "stub_const_spec.rb" with:
+ """ruby
+ module MyGem
+ class SomeClass
+ FOO = 7
+ end
+ end
+
+ module MyGem
+ describe SomeClass do
+ it "stubs the nested constant when it is fully qualified" do
+ stub_const("MyGem::SomeClass::FOO", 5)
+ SomeClass::FOO.should eq(5)
+ end
+ end
+ end
+ """
+ When I run `rspec stub_const_spec.rb`
+ Then the examples should all pass
+
+ Scenario: Transfer nested constants
+ Given a file named "stub_const_spec.rb" with:
+ """ruby
+ module MyGem
+ class SomeClass
+ FOO = 7
+ end
+ end
+
+ module MyGem
+ describe SomeClass do
+ let(:fake_class) { Class.new }
+
+ it "does not transfer nested constants by default" do
+ stub_const("MyGem::SomeClass", fake_class)
+ expect { SomeClass::FOO }.to raise_error(NameError)
+ end
+
+ it "transfers nested constants when using :transfer_nested_constants => true" do
+ stub_const("MyGem::SomeClass", fake_class, :transfer_nested_constants => true)
+ SomeClass::FOO.should eq(7)
+ end
+
+ it "can specify a list of nested constants to transfer" do
+ stub_const("MyGem::SomeClass", fake_class, :transfer_nested_constants => [:FOO])
+ SomeClass::FOO.should eq(7)
+ end
+ end
+ end
+ """
+ When I run `rspec stub_const_spec.rb`
+ Then the examples should all pass
+
View
50 features/stubbing_constants/stub_undefined_constant.feature
@@ -0,0 +1,50 @@
+Feature: Stub Undefined Constant
+
+ Use `stub_const` to stub constants. When the constant is not already defined,
+ all the necessary intermediary modules will be dynamically created. When the
+ example completes, the intermediary module constants will be removed to return
+ the constant state to how it started.
+
+ Scenario: Stub top-level constant
+ Given a file named "stub_const_spec.rb" with:
+ """ruby
+ describe "stubbing FOO" do
+ it "can stub undefined constant FOO" do
+ stub_const("FOO", 5)
+ FOO.should eq(5)
+ end
+
+ it "undefines the constant when the example completes" do
+ expect { FOO }.to raise_error(NameError)
+ end
+ end
+ """
+ When I run `rspec stub_const_spec.rb`
+ Then the examples should all pass
+
+ Scenario: Stub nested constant
+ Given a file named "stub_const_spec.rb" with:
+ """ruby
+ module MyGem
+ class SomeClass
+ end
+ end
+
+ module MyGem
+ describe SomeClass do
+ it "can stub an arbitrarily deep constant that is undefined" do
+ defined?(SomeClass::A).should be_false
+ stub_const("MyGem::SomeClass::A::B::C", 3)
+ SomeClass::A::B::C.should eq(3)
+ SomeClass::A.should be_a(Module)
+ end
+
+ it 'undefines the intermediary constants that were dynamically created' do
+ defined?(SomeClass).should be_true
+ defined?(SomeClass::A).should be_false
+ end
+ end
+ end
+ """
+ When I run `rspec stub_const_spec.rb`
+ Then the examples should all pass
View
41 lib/rspec/mocks/example_methods.rb
@@ -41,6 +41,47 @@ def allow_message_expectations_on_nil
Proxy.allow_message_expectations_on_nil
end
+ # Stubs the named constant with the given value.
+ # Like method stubs, the constant will be restored
+ # to its original value (or lack of one, if it was
+ # undefined) when the example completes.
+ #
+ # @param constant_name [String] The fully qualified name of the constant. The current
+ # constant scoping at the point of call is not considered.
+ # @param value [Object] The value to make the constant refer to. When the
+ # example completes, the constant will be restored to its prior state.
+ # @param options [Hash] Stubbing options.
+ # @option options :transfer_nested_constants [Boolean, Array<Symbol>] Determines
+ # what nested constants, if any, will be transferred from the original value
+ # of the constant to the new value of the constant. This only works if both
+ # the original and new values are modules (or classes).
+ # @return [Object] the stubbed value of the constant
+ #
+ # @example
+ #
+ # stub_const("MyClass", Class.new) # => Replaces (or defines) MyClass with a new class object.
+ # stub_const("SomeModel::PER_PAGE", 5) # => Sets SomeModel::PER_PAGE to 5.
+ #
+ # class CardDeck
+ # SUITS = [:Spades, :Diamonds, :Clubs, :Hearts]
+ # NUM_CARDS = 52
+ # end
+ #
+ # stub_const("CardDeck", Class.new)
+ # CardDeck::SUITS # => uninitialized constant error
+ # CardDeck::NUM_CARDS # => uninitialized constant error
+ #
+ # stub_const("CardDeck", Class.new, :transfer_nested_constants => true)
+ # CardDeck::SUITS # => our suits array
+ # CardDeck::NUM_CARDS # => 52
+ #
+ # stub_const("CardDeck", Class.new, :transfer_nested_constants => [:SUITS])
+ # CardDeck::SUITS # => our suits array
+ # CardDeck::NUM_CARDS # => uninitialized constant error
+ def stub_const(constant_name, value, options = {})
+ ConstantStubber.stub(constant_name, value, options)
+ end
+
private
def declare_double(declared_as, *args)
View
1 lib/rspec/mocks/framework.rb
@@ -17,3 +17,4 @@
require 'rspec/mocks/space'
require 'rspec/mocks/serialization'
require 'rspec/mocks/any_instance'
+require 'rspec/mocks/stub_const'
View
280 lib/rspec/mocks/stub_const.rb
@@ -0,0 +1,280 @@
+module RSpec
+ module Mocks
+ # Provides recursive constant lookup methods useful for
+ # constant stubbing.
+ # @api private
+ module RecursiveConstMethods
+ def recursive_const_get(name)
+ name.split('::').inject(Object) { |mod, name| mod.const_get name }
+ end
+
+ def recursive_const_defined?(name)
+ name.split('::').inject([Object, '']) do |(mod, full_name), name|
+ yield(full_name, name) if block_given? && !mod.is_a?(Module)
+ return false unless mod.const_defined?(name)
+ [mod.const_get(name), [mod, name].join('::')]
+ end
+ end
+ end
+
+ # Provides information about constants that may (or may not)
+ # have been stubbed by rspec-mocks.
+ class Constant
+ extend RecursiveConstMethods
+
+ # @api private
+ def initialize(name)
+ @name = name
+ end
+
+ # @return [String] The fully qualified name of the constant.
+ attr_reader :name
+
+ # @return [Object, nil] The original value (e.g. before it
+ # was stubbed by rspec-mocks) of the constant, or
+ # nil if the constant was not previously defined.
+ attr_accessor :original_value
+
+ # @api private
+ attr_writer :previously_defined, :stubbed
+
+ # @return [Boolean] Whether or not the constant was defined
+ # before the current example.
+ def previously_defined?
+ @previously_defined
+ end
+
+ # @return [Boolean] Whether or not rspec-mocks has stubbed
+ # this constant.
+ def stubbed?
+ @stubbed
+ end
+
+ def to_s
+ "#<#{self.class.name} #{name}>"
+ end
+ alias inspect to_s
+
+ # @api private
+ def self.unstubbed(name)
+ const = new(name)
+ const.previously_defined = recursive_const_defined?(name)
+ const.stubbed = false
+ const.original_value = recursive_const_get(name) if const.previously_defined?
+
+ const
+ end
+ private_class_method :unstubbed
+
+ # Queries rspec-mocks to find out information about the named constant.
+ #
+ # @param [String] name the name of the constant
+ # @return [Constant] an object contaning information about the named
+ # constant.
+ def self.original(name)
+ stubber = ConstantStubber.find(name)
+ stubber ? stubber.to_constant : unstubbed(name)
+ end
+ end
+
+ # Provides a means to stub constants.
+ class ConstantStubber
+ extend RecursiveConstMethods
+
+ # Stubs a constant.
+ #
+ # @param (see ExampleMethods#stub_const)
+ # @option (see ExampleMethods#stub_const)
+ # @return (see ExampleMethods#stub_const)
+ #
+ # @see ExampleMethods#stub_const
+ # @note It's recommended that you use `stub_const` in your
+ # examples. This is an alternate public API that is provided
+ # so you can stub constants in other contexts (e.g. helper
+ # classes).
+ def self.stub(constant_name, value, options = {})
+ stubber = if recursive_const_defined?(constant_name, &raise_on_invalid_const)
+ DefinedConstantReplacer
+ else
+ UndefinedConstantSetter
+ end
+
+ stubber = stubber.new(constant_name, value, options[:transfer_nested_constants])
+ stubbers << stubber
+
+ stubber.stub
+ ensure_registered_with_mocks_space
+ value
+ end
+
+ # Contains common functionality used by both of the constant stubbers.
+ #
+ # @api private
+ class BaseStubber
+ include RecursiveConstMethods
+
+ attr_reader :original_value, :full_constant_name
+
+ def initialize(full_constant_name, stubbed_value, transfer_nested_constants)
+ @full_constant_name = full_constant_name
+ @stubbed_value = stubbed_value
+ @transfer_nested_constants = transfer_nested_constants
+ @context_parts = @full_constant_name.split('::')
+ @const_name = @context_parts.pop
+ end
+
+ def to_constant
+ const = Constant.new(full_constant_name)
+ const.stubbed = true
+ const.previously_defined = previously_defined?
+ const.original_value = original_value
+
+ const
+ end
+ end
+
+ # Replaces a defined constant for the duration of an example.
+ #
+ # @api private
+ class DefinedConstantReplacer < BaseStubber
+ def stub
+ @context = recursive_const_get(@context_parts.join('::'))
+ @original_value = @context.const_get(@const_name)
+
+ constants_to_transfer = verify_constants_to_transfer!
+
+ @context.send(:remove_const, @const_name)
+ @context.const_set(@const_name, @stubbed_value)
+
+ transfer_nested_constants(constants_to_transfer)
+ end
+
+ def previously_defined?
+ true
+ end
+
+ def rspec_reset
+ @context.send(:remove_const, @const_name)
+ @context.const_set(@const_name, @original_value)
+ end
+
+ def transfer_nested_constants(constants)
+ constants.each do |const|
+ @stubbed_value.const_set(const, original_value.const_get(const))
+ end
+ end
+
+ def verify_constants_to_transfer!
+ return [] unless @transfer_nested_constants
+
+ { @original_value => "the original value", @stubbed_value => "the stubbed value" }.each do |value, description|
+ unless value.respond_to?(:constants)
+ raise ArgumentError,
+ "Cannot transfer nested constants for #{@full_constant_name} " +
+ "since #{description} is not a class or module and only classes " +
+ "and modules support nested constants."
+ end
+ end
+
+ if @transfer_nested_constants.is_a?(Array)
+ @transfer_nested_constants = @transfer_nested_constants.map(&:to_s) if RUBY_VERSION == '1.8.7'
+ undefined_constants = @transfer_nested_constants - @original_value.constants
+
+ if undefined_constants.any?
+ available_constants = @original_value.constants - @transfer_nested_constants
+ raise ArgumentError,
+ "Cannot transfer nested constant(s) #{undefined_constants.join(' and ')} " +
+ "for #{@full_constant_name} since they are not defined. Did you mean " +
+ "#{available_constants.join(' or ')}?"
+ end
+
+ @transfer_nested_constants
+ else
+ @original_value.constants
+ end
+ end
+ end
+
+ # Sets an undefined constant for the duration of an example.
+ #
+ # @api private
+ class UndefinedConstantSetter < BaseStubber
+ def stub
+ remaining_parts = @context_parts.dup
+ @deepest_defined_const = @context_parts.inject(Object) do |klass, name|
+ break klass unless klass.const_defined?(name)
+ remaining_parts.shift
+ klass.const_get(name)
+ end
+
+ context = remaining_parts.inject(@deepest_defined_const) do |klass, name|
+ klass.const_set(name, Module.new)
+ end
+
+ @const_to_remove = remaining_parts.first || @const_name
+ context.const_set(@const_name, @stubbed_value)
+ end
+
+ def previously_defined?
+ false
+ end
+
+ def rspec_reset
+ @deepest_defined_const.send(:remove_const, @const_to_remove)
+ end
+ end
@justinko
justinko added a note

Looks like UndefinedConstantSetter and DefinedConstantReplacer share some functionality. Here is a parent class they could share:

class StubbableConstant
  attr_reader :original_value, :full_constant_name

  def initialize(full_constant_name, stubbed_value)
    @full_constant_name = full_constant_name
    @stubbed_value = stubbed_value
  end

  # methods that #stub can use, which would "thin" it out a bit
  private

  def context_parts
    @full_constant_name.split('::')
  end

  def const_name
    @const_name ||= context_parts.pop
  end
end

Which would allow:

class DefinedConstantReplacer < StubbableConstant
  def initialize(full_constant_name, stubbed_value, transfer_nested_constants)
    super(full_constant_name, stubbed_value)
    @transfer_nested_constants = transfer_nested_constants
  end
end
@myronmarston RSpec member

Thanks, I just pushed a slightly different version of what you suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ # Ensures the constant stubbing is registered with
+ # rspec-mocks space so that stubbed constants can
+ # be restored when examples finish.
+ #
+ # @api private
+ def self.ensure_registered_with_mocks_space
+ return if @registered_with_mocks_space
+ ::RSpec::Mocks.space.add(self)
+ @registered_with_mocks_space = true
+ end
+
+ # Resets all stubbed constants. This is called automatically
+ # by rspec-mocks when an example finishes.
+ #
+ # @api private
+ def self.rspec_reset
+ @registered_with_mocks_space = false
+
+ # We use reverse order so that if the same constant
+ # was stubbed multiple times, the original value gets
+ # properly restored.
+ stubbers.reverse.each { |s| s.rspec_reset }
+
+ stubbers.clear
+ end
+
+ # The list of constant stubbers that have been used for
+ # the current example.
+ #
+ # @api private
+ def self.stubbers
+ @stubbers ||= []
+ end
+
+ def self.find(name)
+ stubbers.find { |s| s.full_constant_name == name }
+ end
+
+ # Used internally by the constant stubbing to raise a helpful
+ # error when a constant like "A::B::C" is stubbed and A::B is
+ # not a module (and thus, it's impossible to define "A::B::C"
+ # since only modules can have nested constants).
+ #
+ # @api private
+ def self.raise_on_invalid_const
+ lambda do |const_name, failed_name|
+ raise "Cannot stub constant #{failed_name} on #{const_name} " +
+ "since #{const_name} is not a module."
+ end
+ end
+ end
+ end
+end
+
View
309 spec/rspec/mocks/stub_const_spec.rb
@@ -0,0 +1,309 @@
+require 'spec_helper'
+
+TOP_LEVEL_VALUE_CONST = 7
+
+class TestClass
+ M = :m
+ N = :n
+
+ class Nested
+ class NestedEvenMore
+ end
+ end
+end
+
+module RSpec
+ module Mocks
+ describe "Constant Stubbing" do
+ include RSpec::Mocks::RecursiveConstMethods
+
+ def reset_rspec_mocks
+ ::RSpec::Mocks.space.reset_all
+ end
+
+ shared_examples_for "loaded constant stubbing" do |const_name|
+ let!(:original_const_value) { const }
+ after { change_const_value_to(original_const_value) }
+
+ define_method :const do
+ recursive_const_get(const_name)
+ end
+
+ define_method :parent_const do
+ recursive_const_get("Object::" + const_name.sub(/(::)?[^:]+\z/, ''))
+ end
+
+ define_method :last_const_part do
+ const_name.split('::').last
+ end
@justinko
justinko added a note

You can get rid of the duplication of these "helper" methods by two ways:

module ConstNameHelpers
  include RSpec::Mocks::RecursiveConstMethods

  def const
    recursive_const_get(self)
  end
end

shared_examples_for "unloaded constant stubbing" do |const_name|
  const_name.extend ConstNameHelpers

  it 'allows it to be stubbed' do
    const_name.const.should_not eq(7)
  end
end

OR

module ConstHelpers
  include RSpec::Mocks::RecursiveConstMethods

  def self.[](const_name)
    @@const_name = const_name
    self
  end

  def const
    recursive_const_get(@@const_name)
  end
end

shared_examples_for "unloaded constant stubbing" do |const_name|
  include ConstHelpers[const_name]

  it 'allows it to be stubbed' do
    const.should_not eq(7)
  end
end
@justinko
justinko added a note

You could also nest an include_examples (although you'd have to make it support a block, or just use shared_examples):

shared_examples_for "unloaded constant stubbing" do |const_name|
  include_examples 'const helpers', const_name

  it 'allows it to be stubbed' do
    const.should_not eq(7)
  end
end

This might be considered "abuse" though....

@myronmarston RSpec member

I think all of these reduce the clarity of the code significantly, so I'm going to stick with what I have, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ def change_const_value_to(value)
+ parent_const.send(:remove_const, last_const_part)
+ parent_const.const_set(last_const_part, value)
+ end
+
+ it 'allows it to be stubbed' do
+ const.should_not eq(7)
+ stub_const(const_name, 7)
+ const.should eq(7)
+ end
+
+ it 'resets it to its original value when rspec clears its mocks' do
+ original_value = const
+ original_value.should_not eq(:a)
+ stub_const(const_name, :a)
+ reset_rspec_mocks
+ const.should be(original_value)
+ end
+
+ it 'returns the stubbed value' do
+ orig_value = const
+ stub_const(const_name, 7).should eq(7)
+ end
+ end
+
+ shared_examples_for "unloaded constant stubbing" do |const_name|
+ before { recursive_const_defined?(const_name).should be_false }
+
+ define_method :const do
+ recursive_const_get(const_name)
+ end
+
+ define_method :parent_const do
+ recursive_const_get("Object::" + const_name.sub(/(::)?[^:]+\z/, ''))
+ end
+
+ define_method :last_const_part do
+ const_name.split('::').last
+ end
+
+ it 'allows it to be stubbed' do
+ stub_const(const_name, 7)
+ const.should eq(7)
+ end
+
+ it 'removes the constant when rspec clears its mocks' do
+ stub_const(const_name, 7)
+ reset_rspec_mocks
+ recursive_const_defined?(const_name).should be_false
+ end
+
+ it 'returns the stubbed value' do
+ stub_const(const_name, 7).should eq(7)
+ end
+
+ it 'ignores the :transfer_nested_constants option if passed' do
+ stub = Module.new
+ stub_const(const_name, stub, :transfer_nested_constants => true)
+ stub.constants.should eq([])
+ end
+ end
+
+ describe "#stub_const" do
+ context 'for a loaded unnested constant' do
+ it_behaves_like "loaded constant stubbing", "TestClass"
+
+ it 'can be stubbed multiple times but still restores the original value properly' do
+ orig_value = TestClass
+ stub1, stub2 = Module.new, Module.new
+ stub_const("TestClass", stub1)
+ stub_const("TestClass", stub2)
+
+ reset_rspec_mocks
+ TestClass.should be(orig_value)
+ end
+
+ it 'allows nested constants to be transferred to a stub module' do
+ tc_nested = TestClass::Nested
+ stub = Module.new
+ stub_const("TestClass", stub, :transfer_nested_constants => true)
+ stub::M.should eq(:m)
+ stub::N.should eq(:n)
+ stub::Nested.should be(tc_nested)
+ end
+
+ it 'allows nested constants to be selectively transferred to a stub module' do
+ stub = Module.new
+ stub_const("TestClass", stub, :transfer_nested_constants => [:M, :N])
+ stub::M.should eq(:m)
+ stub::N.should eq(:n)
+ defined?(stub::Nested).should be_false
+ end
+
+ it 'raises an error if asked to transfer nested constants but given an object that does not support them' do
+ original_tc = TestClass
+ stub = Object.new
+ expect {
+ stub_const("TestClass", stub, :transfer_nested_constants => true)
+ }.to raise_error(ArgumentError)
+
+ TestClass.should be(original_tc)
+
+ expect {
+ stub_const("TestClass", stub, :transfer_nested_constants => [:M])
+ }.to raise_error(ArgumentError)
+
+ TestClass.should be(original_tc)
+ end
+
+ it 'raises an error if asked to transfer nested constants on a constant that does not support nested constants' do
+ stub = Module.new
+ expect {
+ stub_const("TOP_LEVEL_VALUE_CONST", stub, :transfer_nested_constants => true)
+ }.to raise_error(ArgumentError)
+
+ TOP_LEVEL_VALUE_CONST.should eq(7)
+
+ expect {
+ stub_const("TOP_LEVEL_VALUE_CONST", stub, :transfer_nested_constants => [:M])
+ }.to raise_error(ArgumentError)
+
+ TOP_LEVEL_VALUE_CONST.should eq(7)
+ end
+
+ it 'raises an error if asked to transfer a nested constant that is not defined' do
+ original_tc = TestClass
+ defined?(TestClass::V).should be_false
+ stub = Module.new
+
+ expect {
+ stub_const("TestClass", stub, :transfer_nested_constants => [:V])
+ }.to raise_error(/cannot transfer nested constant.*V/i)
+
+ TestClass.should be(original_tc)
+ end
+ end
+
+ context 'for a loaded nested constant' do
+ it_behaves_like "loaded constant stubbing", "TestClass::Nested"
+ end
+
+ context 'for a loaded deeply nested constant' do
+ it_behaves_like "loaded constant stubbing", "TestClass::Nested::NestedEvenMore"
+ end
+
+ context 'for an unloaded unnested constant' do
+ it_behaves_like "unloaded constant stubbing", "X"
+ end
+
+ context 'for an unloaded nested constant' do
+ it_behaves_like "unloaded constant stubbing", "X::Y"
+
+ it 'removes the root constant when rspec clears its mocks' do
+ defined?(X).should be_false
+ stub_const("X::Y", 7)
+ reset_rspec_mocks
+ defined?(X).should be_false
+ end
+ end
+
+ context 'for an unloaded deeply nested constant' do
+ it_behaves_like "unloaded constant stubbing", "X::Y::Z"
+
+ it 'removes the root constant when rspec clears its mocks' do
+ defined?(X).should be_false
+ stub_const("X::Y::Z", 7)
+ reset_rspec_mocks
+ defined?(X).should be_false
+ end
+ end
+
+ context 'for an unloaded constant nested within a loaded constant' do
+ it_behaves_like "unloaded constant stubbing", "TestClass::X"
+
+ it 'removes the unloaded constant but leaves the loaded constant when rspec resets its mocks' do
+ defined?(TestClass).should be_true
+ defined?(TestClass::X).should be_false
+ stub_const("TestClass::X", 7)
+ reset_rspec_mocks
+ defined?(TestClass).should be_true
+ defined?(TestClass::X).should be_false
+ end
+
+ it 'raises a helpful error if it cannot be stubbed due to an intermediary constant that is not a module' do
+ TestClass::M.should be_a(Symbol)
+ expect { stub_const("TestClass::M::X", 5) }.to raise_error(/cannot stub/i)
+ end
+ end
+
+ context 'for an unloaded constant nested deeply within a deeply nested loaded constant' do
+ it_behaves_like "unloaded constant stubbing", "TestClass::Nested::NestedEvenMore::X::Y::Z"
+
+ it 'removes the first unloaded constant but leaves the loaded nested constant when rspec resets its mocks' do
+ defined?(TestClass::Nested::NestedEvenMore).should be_true
+ defined?(TestClass::Nested::NestedEvenMore::X).should be_false
+ stub_const("TestClass::Nested::NestedEvenMore::X::Y::Z", 7)
+ reset_rspec_mocks
+ defined?(TestClass::Nested::NestedEvenMore).should be_true
+ defined?(TestClass::Nested::NestedEvenMore::X).should be_false
+ end
+ end
+ end
+ end
+
+ describe Constant do
+ describe ".original" do
+ context 'for a previously defined unstubbed constant' do
+ let(:const) { Constant.original("TestClass::M") }
+
+ it("exposes its name") { const.name.should eq("TestClass::M") }
+ it("indicates it was previously defined") { const.should be_previously_defined }
+ it("indicates it has not been stubbed") { const.should_not be_stubbed }
+ it("exposes its original value") { const.original_value.should eq(:m) }
+ end
+
+ context 'for a previously defined stubbed constant' do
+ before { stub_const("TestClass::M", :other) }
+ let(:const) { Constant.original("TestClass::M") }
+
+ it("exposes its name") { const.name.should eq("TestClass::M") }
+ it("indicates it was previously defined") { const.should be_previously_defined }
+ it("indicates it has been stubbed") { const.should be_stubbed }
+ it("exposes its original value") { const.original_value.should eq(:m) }
+ end
+
+ context 'for a previously undefined stubbed constant' do
+ before { stub_const("TestClass::Undefined", :other) }
+ let(:const) { Constant.original("TestClass::Undefined") }
+
+ it("exposes its name") { const.name.should eq("TestClass::Undefined") }
+ it("indicates it was not previously defined") { const.should_not be_previously_defined }
+ it("indicates it has been stubbed") { const.should be_stubbed }
+ it("returns nil for the original value") { const.original_value.should be_nil }
+ end
+
+ context 'for a previously undefined unstubbed constant' do
+ let(:const) { Constant.original("TestClass::Undefined") }
+
+ it("exposes its name") { const.name.should eq("TestClass::Undefined") }
+ it("indicates it was not previously defined") { const.should_not be_previously_defined }
+ it("indicates it has not been stubbed") { const.should_not be_stubbed }
+ it("returns nil for the original value") { const.original_value.should be_nil }
+ end
+
+ context 'for a previously defined constant that has been stubbed twice' do
+ before { stub_const("TestClass::M", 1) }
+ before { stub_const("TestClass::M", 2) }
+ let(:const) { Constant.original("TestClass::M") }
+
+ it("exposes its name") { const.name.should eq("TestClass::M") }
+ it("indicates it was previously defined") { const.should be_previously_defined }
+ it("indicates it has been stubbed") { const.should be_stubbed }
+ it("exposes its original value") { const.original_value.should eq(:m) }
+ end
+
+ context 'for a previously undefined constant that has been stubbed twice' do
+ before { stub_const("TestClass::Undefined", 1) }
+ before { stub_const("TestClass::Undefined", 2) }
+ let(:const) { Constant.original("TestClass::Undefined") }
+
+ it("exposes its name") { const.name.should eq("TestClass::Undefined") }
+ it("indicates it was not previously defined") { const.should_not be_previously_defined }
+ it("indicates it has been stubbed") { const.should be_stubbed }
+ it("returns nil for the original value") { const.original_value.should be_nil }
+ end
+ end
+ end
+ end
+end
+
Something went wrong with that request. Please try again.