Constant Stubbing in RSpec 2.11

Myron Marston

Jun 29, 2012

In the 2.11 release, rspec-mocks is gaining a significant new capability that, as far as I know, isn’t provided by any other ruby mocking library: constant stubbing[^foot_1].

Let’s look at the API, and then talk a bit about some of the use cases for it.

The API

The main API is stub_const:

describe "stub_const" do
  it "changes the constant value for the duration of the example" do
    stub_const("Foo::SIZE", 10)
    expect(Foo::SIZE).to eq(10)
  end
end

This works for both defined and undefined constants; you could stub A::B::C::D::E::F even if none of the intermediary constants exist. When the example completes, the constants will be restored to their original states: any newly defined constants will be undefined, and any modified constants will be restored to their original values.

Note that constant names must be fully qualified; the current module nesting is not considered:

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

stub_const also supports a :transfer_nested_constants option. Consider a case where you have nested constants:

class CardDeck
  SUITS = [:spades, :diamonds, :clubs, :hearts]
  NUM_CARDS = 52
end

stub_const("CardDeck", fake_class) cuts off access to the nested constants (CardDeck::SUITS and CardDeck::NUM_CARDS), unless you manually assign fake_class::SUITS and fake_class::NUM_CARDS. The :transfer_nested_constants option is provided to take care of this for you:

# Default behavior:
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

# `:transfer_nested_constants => true` transfers all nested constants:
stub_const("CardDeck", fake_class, :transfer_nested_constants => true)
CardDeck::SUITS # => [:spades, :diamonds, :clubs, :hearts]
CardDeck::NUM_CARDS # => 52

# Or you can specify a list of constants to transfer:
stub_const("CardDeck", fake_class, :transfer_nested_constants => [:SUITS])
CardDeck::SUITS # => [:spades, :diamonds, :clubs, :hearts]
CardDeck::NUM_CARDS # => raises uninitialized constant error

Use Cases

I’ve found this useful in a few different situations:

If you’re curious how it all works, check out the source on github.

[^foot_1]: Actually, this has been available in rspec-fire for a good four months or so. But it’s not really a full mocking library…it builds on top of rspec-mocks, and now this functionality has been ported over to rspec-mocks.