skip to content

Doubles & Message Stubs

rspec-mocks' plain and verifying doubles, allow and expect with receive, spies checked with have_received, and stub_const. Interviewers probe when a double hides a real interface change.

on this pageshow

explore

questions

6

In rspec-mocks, what is the difference between allow(obj).to receive(:msg) and expect(obj).to receive(:msg)?

level: juniorimportance: must knowfreq 72%

answer

  1. one permits, one demands
  2. allowed message returns nil
  3. checked when the example ends
  4. must be set before the call
  5. exactly once unless told otherwise

basics

~20 s

allow(obj).to receive(:msg) permits a message and configures its reply; the example passes whether or not it is sent. expect(obj).to receive(:msg) also demands it: unless it arrives exactly once by the end of the example, the example fails.

solid answer

~40 s

Both take the same `receive(:msg)` object with the same fluent interface (`with`, `and_return`, `and_raise`, count constraints), and both work on a plain `double` or on a real object. `allow` only **permits** the message: with no response configured it returns `nil`, and nothing checks whether it was ever sent. `expect(...).to receive` **also sets a message expectation** that rspec-mocks verifies when the example finishes: by default the message must arrive **exactly once**, otherwise the failure reads `expected: 1 time ... received: 0 times`. Because the expectation is registered up front, it has to be written **before** the code under test runs. Use `allow` for a collaborator whose answer you need, `expect` only for the call that is the behaviour you are testing, and `expect(...).not_to receive` when a message must never be sent.

code

ruby · 14 lines
ruby
RSpec.describe NotificationService do
  let(:gateway) { double("sms gateway") }
  let(:service) { NotificationService.new(gateway:) }

  it "texts the user" do
    expect(gateway).to receive(:deliver).with(to: "+15550100", body: "Hi")
    service.notify(phone: "+15550100", text: "Hi")
  end

  it "records the receipt the gateway returns" do
    allow(gateway).to receive(:deliver).and_return("msg-42")
    expect(service.notify(phone: "+15550100", text: "Hi")).to eq("msg-42")
  end
end

go deeper

for a junior

Remember the one-line rule: allow permits and supplies an answer, expect also demands the call. Know that an un-configured stub returns nil and that expect must be set before the action.

for a middle

Explain that expect registers a message expectation verified when the example ends, that the default count is exactly once, and how with, once, twice and at_least change what is checked.

for a senior

Show judgement about how many expects an example carries: one demanded call per behaviour, allow for everything incidental, so refactors that change call counts do not break unrelated examples.

for a principal

Frame the choice as a suite-wide convention: reviewers should question examples whose message expectations restate the implementation, because that coupling is what makes a large RSpec suite expensive to change.

## Two verbs, one message builder rspec-mocks, the mocking library that ships with RSpec 3.13, lets a spec configure how an object reacts to a **message** (a method call). The configuration always has the same shape: ```ruby allow(gateway).to receive(:deliver).and_return(true) expect(gateway).to receive(:deliver).with(to: "+15550100", body: "Hi") ``` `receive(:deliver)` builds the description of the message; the verb in front of it decides what RSpec does with that description. The object can be a **pure test double** made with `double` or `instance_double`, or a **partial double**, meaning a real object (or class) with just this one method replaced for the duration of the example. ## What allow does `allow(obj).to receive(:msg)` **permits** the message: - a strict `double` that would otherwise raise `received unexpected message :msg` now accepts it; - the reply is whatever you configure (`and_return`, `and_raise`, a block), and **`nil`** if you configure nothing; - nothing is checked at the end of the example; zero calls or fifty calls both pass. That makes `allow` the tool for a collaborator whose *answer* the code under test needs, such as a gateway that must return a delivery receipt so the service reaches its next branch. ## What expect ... to receive adds `expect(obj).to receive(:msg)` does everything `allow` does and also registers a **message expectation**. When the example completes, rspec-mocks verifies every registered expectation: 1. by default the message must have been received **exactly once**; zero calls and two calls both fail; 2. `with(...)` constrains the arguments; a call with other arguments fails, reported as `with unexpected arguments`, unless an `allow` stub for the same message catches it; 3. count constraints change the rule: `once`, `twice`, `thrice`, `exactly(3).times`, `at_least(:once)`, `at_most(2).times`. A failed expectation is reported like this: ``` (Double "sms gateway").deliver(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments ``` `expect(obj).not_to receive(:msg)` is the negative form: any receipt of the message fails the example immediately. ## Order matters: expectation first, action second Because `expect ... to receive` installs its expectation when that line runs, it must come **before** the code under test. Written after the call, it installs a fresh expectation that nothing will satisfy, and the example fails with `received: 0 times` even though the call really happened. If you prefer to assert after acting, the spy style (`allow` or `spy`, then `expect(obj).to have_received(:msg)`) exists for exactly that. ## Side by side | | `allow(obj).to receive(:msg)` | `expect(obj).to receive(:msg)` | |---|---|---| | Unexpected-message error on a double | suppressed | suppressed | | Default return value | `nil` | `nil` | | Verified at end of example | no | yes, exactly once by default | | Placement | anywhere before the call | before the call | | Typical use | supply a collaborator's answer | assert the call that *is* the behaviour | ## How a failure surfaces When a strict double receives a message nobody allowed, or a message expectation is not met, rspec-mocks raises `RSpec::Mocks::MockExpectationError`. That class inherits from **`Exception`**, not `StandardError`, which matters for code under test that protects itself with a bare `rescue => e`: - a bare `rescue` only catches `StandardError` and its subclasses; - so a service that wraps its gateway call in `rescue => e` and logs the error does **not** swallow the mock failure; - the example still fails with the rspec-mocks message instead of passing silently. Unmet `expect ... to receive` expectations are checked after the example body has run, so the failure is reported against the line that set the expectation, which is why reading the `Failure/Error:` line points you at the `expect`, not at the production call. ## Choosing between them A useful rule is that each example should demand only what it is about. In a notification-service spec, the example "sends the text through the SMS gateway" earns one `expect(gateway).to receive(:deliver)`; every other example that merely needs the gateway to cooperate uses `allow`. Turning every `allow` into `expect` welds the spec to the implementation: harmless refactors that change how often a collaborator is asked start failing examples that were never about that call. ## Common mistakes - Writing `expect(...).to receive` after the action and expecting it to check the past. - Believing `allow` fails when the message is never sent; it never checks. - Forgetting that an un-configured stub returns `nil`, then chasing a `NoMethodError` on `nil` further down. - Assuming a plain `expect(...).to receive` accepts any number of calls; the default is exactly one.

  • What happens if the code under test calls a message twice that was set up with a plain expect(obj).to receive(:msg)?
    The example fails. A message expectation with no count constraint expects exactly one call, so the second call leaves the actual count at 2 and verification at the end of the example reports `expected: 1 time ... received: 2 times`. Add `twice`, `exactly(n).times` or `at_least(:once)` when more than one call is legitimate.
  • Can allow and expect be combined on the same message in one example?
    Yes. A common pattern is a broad `allow(gateway).to receive(:deliver).and_return(true)` in a `before` hook, then `expect(gateway).to receive(:deliver).with(to: "+15550100", body: anything)` inside the one example that is about that call. The expectation is checked, and calls it does not match fall back to the allowed stub.

saying these in an interview costs you the question

  • allow(...).to receive fails the example when the message is never sent
  • expect(...).to receive can be written after the action and still check it
  • an allowed message with no and_return returns the real method's result
  • a plain expect(...).to receive accepts any number of calls
  • every collaborator call should be an expect so nothing slips through
open as a page

In an RSpec spec, how does instance_double differ from a plain double, and what does it check against the real class?

level: middleimportance: must knowfreq 62%

basics

~20 s

A plain double accepts stubs for any method name, so it stays green when the real class changes. instance_double(SmsGateway) only lets you stub or expect instance methods SmsGateway really defines, and checks calls against their parameters.

open as a page

In rspec-mocks, what do and_return, and_raise and and_call_original make a stubbed message do, and where is and_call_original unavailable?

level: middleimportance: should knowfreq 45%

basics

~20 s

and_return sets the reply, and with several values returns them in order, repeating the last. and_raise raises an exception class or instance. and_call_original runs the real method, so it works only on partial doubles, never on a pure double.

open as a page

In an RSpec spec, how does asserting with spy and have_received differ from expect(...).to receive, and when does have_received refuse to work?

level: middleimportance: should knowfreq 48%

basics

~10 s

expect(...).to receive is set before the action; have_received checks afterwards that a recorded message arrived. It needs a spy or a message allowed beforehand, otherwise it fails: not a spy or method not stubbed.

open as a page

An RSpec 3.13 spec stubs SmsGateway.deliver on the real class and still passes after deliver was renamed; what does verify_partial_doubles change, and what does it still miss?

level: seniorimportance: should knowfreq 38%

basics

~20 s

With verify_partial_doubles off, which is rspec-mocks' default in RSpec 3.13, a real object can be stubbed with methods it no longer has. Turned on, stubs are checked for existence and argument signature; return values and pure doubles stay unchecked.

open as a page

In an RSpec spec, what does stub_const do, and why can stub_const("SmsGateway", fake) miss the constant the code actually uses?

level: middleimportance: nice to knowfreq 26%

basics

~10 s

stub_const replaces or defines a constant for one example and restores it afterwards. Its name must be fully qualified: inside module Notifications, stub_const("SmsGateway", fake) stubs ::SmsGateway, not Notifications::SmsGateway.

open as a page