Constant Stubbing in RSpec 2.11

Myron Marston

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 .

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:

It provides a simple way to change a class setting expressed as a constant for one test. In the past, I've often defined static class methods just so they could be stubbed, even though it made more sense to use a constant. Now you can just use a constant!

It makes dependency injection easy when the class-under-test depends on a collaborator's class method (e.g. when the collaborator is stateless). You can easily stub the collaborator's class constant with a test double.

It makes stubbing unloaded dependencies dead-simple. Gary Bernhardt discussed this situation at length in Destroy all Software #46. He mentioned mutating constants as a possible way of stubbing unloaded dependencies, but recommended against it because of the complexity of safely managing this. Now that rspec-mocks can do it for you, it's far less complex, and much, much safer.

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

Please enable JavaScript to view the comments powered by Disqus.