Code Reading: Message Stubbing in RSpec

RSpec isn't just nested context blocks and expect(...).to expectations; it also includes a powerful mocking library: rspec-mocks.

Take the following spec, which stubs Car#honk:

# car_spec.rb

class Car
  def honk
    puts 'Honk!!'
  end
end

describe Car do
  let(:car) { described_class.new }

  it 'receives honk' do
    # Stubs the `#honk` message and returns `nil`.
    # The actual implementation, which prints "Honk!!",
    # is never executed.
    allow(car).to receive(:honk)
    car.honk
  end
end

How does RSpec achieve this? Let's examine the source code step-by-step.

The allow syntax

allow is defined in RSpec::Mocks::Syntax. As with the should and expect(...).to syntax in rspec-expectations, RSpec::Mocks::Configuration allows a user to enable or disable their preferred message stubbing syntax:

RSpec.configure do |config|
  config.mock_with :rspec do |c|
    # Both `#stub` and `#allow` are enabled by default
    # c.syntax = [:should, :expect]

    # Only enable `#allow`.
    # The old syntax, `car.stub :honk`, raises an exception
    c.syntax = [:expect]
  end
end

As discussed in my slides for "RSpec 3.0: Under the Covers", the new syntax is preferred because, unlike #stub, it does not monkey-patch the Object class. Instead, #allow is defined on RSpec::Mocks::ExampleMethods, which is included in the RSpec namespace.

The allow implementation

#allow returns an AllowanceTarget that calls the #setup_allowance method on whatever matcher is passed to #to–usually the receive matcher:

# Calls `#setup_allowance` on `::Mocks::Matchers::Receive`
allow(car).to receive(:honk)

The extra layer of indirection in the allow(...).to syntax provides some flexibility. For example, AllowanceTarget instances do not accept negation via #not_to or #to_not. That is, allow(...).not_to, which doesn't make very much sense, raises a NegationUnsupportedError.

The receive matcher and message stubbing: proxies in space

RSpec implements message stubbing by maintaining a hash of proxies. The keys in the hash are the object IDs of the objects being proxied to (recall that you can get a unique ID for any Ruby object by using the #__id__ method).

Each proxy maintains an array of MethodDouble objects, which are responsible for intercepting individual messages.

These proxies are stored in instances of RSpec::Mocks::Space. In order to be integrated into testing frameworks like rspec-core, Space needs to be integrated in three steps during each example:

When the receive matcher is sent the #setup_allowance message, it calls #add_stub on the proxy, passing the original method name as a argument. This aliases the original method to be stubbed to a new method called "obfuscated_by_rspec_mocks__#{method}". In its place, it defines a new method with the original method name. This doppelganger method calls #proxy_method_invoked on the MethodDouble that corresponds to the stubbed method.

Bringing this back to our example:

# 1. `receive(:honk)` finds the proxy for the `car` object in
#    the current space. It then adds a message double for `#honk`
#    on that proxy.
# 2. `receive` aliases `#honk` to a new method,
#     `#obfuscated_by_rspec_mocks__honk`.
# 3. `#honk` itself is replaced with a new method of the same name.
#     This new method calls `#proxy_method_invoked` on the
#     message double for `#honk`.

allow(car).to receive(:honk)

After the example is finished, Mocks.verify checks the number of times each proxy's method doubles were invoked, and compares it to any expectations that may have been made. Finally, Mocks.teardown restores all the original methods, returning #obfuscated_by_rspec_mocks__honk to its original name of #honk. All proxies and message doubles are removed.

No black magic, just good design

Pretty neat, huh? I expected to see more black magic, but under the covers it was just a simple method alias and some good object-oriented design. It's thanks to this design that RSpec can trivially support the following:

# car_spec.rb

class Car
  def honk
    puts 'Honk!!'
  end
end

describe Car do
  let(:car) { described_class.new }

  it 'receives honk' do
    # Stubs the message like before, but in addition also calls
    # the original implementation of `#honk`, thereby printing
    # "Honk!!". All this does is call the aliased method,
    # `#obfuscated_by_rspec_mocks__honk`!
    allow(car).to receive(:honk).and_call_original
    car.honk
  end
end

More on RSpec