skip to content

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%

answer

  1. a double knows no class
  2. does not implement the instance method
  3. arity and keywords checked too
  4. class_double for class methods
  5. String name: no check unless loaded

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.

solid answer

~40 s

`double("sms gateway")` is a blank, strict object: it raises on messages you did not allow, but it will let you allow **any** name, so if `SmsGateway#deliver` is renamed the spec keeps passing against a fiction. `instance_double(SmsGateway)` is a **verifying double**: stubbing a method the class does not define fails with `the SmsGateway class does not implement the instance method: ...`, a private or protected method raises `NoMethodError`, and calls and `with(...)` constraints are checked against the real method's signature, including required keywords. `class_double` does the same for class methods and `object_double` for one real object. Passing the class name as a **String** only verifies when that constant is loaded; otherwise it behaves like a plain double unless `verify_doubled_constant_names` is on.

code

ruby · 16 lines
ruby
class SmsGateway
  def deliver(to:, body:) = true
end

RSpec.describe NotificationService do
  it "fails fast when the stub no longer matches the gateway" do
    gateway = instance_double(SmsGateway)
    # the SmsGateway class does not implement the instance method: send_sms
    allow(gateway).to receive(:send_sms)
  end

  it "checks the call against deliver's keywords" do
    gateway = instance_double(SmsGateway, deliver: true)
    gateway.deliver(to: "+15550100") # ArgumentError: Missing required keyword arguments: body
  end
end

go deeper

for a junior

Recall that double knows no class while instance_double is tied to one, and that stubbing a method the class lacks fails with a does-not-implement message.

for a middle

Explain the three checks: existence, visibility and argument signature, plus when to reach for class_double or object_double and why a String name may disable verification.

for a senior

Discuss the limits: return values are not verified, method_missing methods are invisible, and verification needs the class loaded, so the full suite must load it or set verify_doubled_constant_names.

for a principal

Weigh making verifying doubles a team default, backed by a full-suite configuration, against the isolation some fast unit runs rely on, and where integration specs must cover what signatures cannot.

## A plain double is a blank object `double` in rspec-mocks (RSpec 3.13) builds an object that knows nothing about any class. It is **strict** in one sense: sending it a message you have not allowed or expected raises `received unexpected message :name`. But it is **permissive** in the sense that matters for design drift: you can `allow` or `expect` any method name with any arguments, and it will happily answer. That is exactly how a double "hides a real interface change". A notification service spec stubs `deliver(to:, body:)` on a double standing in for `SmsGateway`. Months later someone renames the real method to `send_message` and changes its keywords. Every unit spec for the notification service still passes, because none of them ever touched the real class, and production raises `NoMethodError` on the first text. ## What a verifying double adds `instance_double(SmsGateway)` (or `instance_double("SmsGateway")`) is a **verifying double**. Behaviourally it is still a double, but each stub or expectation is checked against the class: 1. **Existence.** Stubbing or expecting a method that `SmsGateway` does not define as an instance method fails with `the SmsGateway class does not implement the instance method: send_sms`. If the class *does* have a class method of that name, the message adds `Perhaps you meant to use class_double instead?`. 2. **Visibility.** Calling a private or protected method on the double raises `NoMethodError`, as it would on a real instance. 3. **Signature.** Calls on the double are checked against the real method's parameters, so a missing required keyword raises `ArgumentError` (`Missing required keyword arguments: body`), and a `with(...)` constraint whose argument list the real method could never accept fails when the expectation is declared. The rest of the API is unchanged: `allow`, `expect`, `and_return` and `have_received` work the same way. ## The family | Constructor | Verifies against | Typical use | |---|---|---| | `double` | nothing | a throwaway value object with no real counterpart | | `instance_double(Klass)` | public instance methods of `Klass` | a collaborator object passed in | | `class_double(Klass)` | class (singleton) methods of `Klass` | code calling `Klass.fetch` directly | | `object_double(obj)` | the methods one real object responds to | a global or singleton object | | `instance_spy`, `class_spy`, `object_spy` | same as above, as null objects | spy-style checks with `have_received` | ## Reading the failures The errors a verifying double raises name the mismatch precisely, which is most of its value in a code review: | Mistake | What rspec-mocks reports | |---|---| | stubbed name missing from the class | `the SmsGateway class does not implement the instance method: send_sms` | | instance method stubbed on `class_double` | `... does not implement the class method: ... Perhaps you meant to use instance_double instead?` | | call missing a required keyword | `ArgumentError` with `Missing required keyword arguments: body` | | wrong number of positional arguments | `Wrong number of arguments. Expected 1, got 2.` | Each of these would have been a silent pass with a plain `double`. ## The String form and the unloaded class `instance_double` accepts a class **or a String** naming it. The String form exists so that a fast unit spec can run without loading the collaborator's file. The documented trade-off: - if the constant is **loaded** when the double is used, the checks above apply; - if it is **not loaded**, no checking happens at all and the double behaves like a plain `double`; - a typo in the String therefore silently disables verification. Setting `verify_doubled_constant_names = true` in the rspec-mocks configuration turns an undefined name into an error. The rspec-mocks documentation advises enabling it only for full-suite runs where all production code is loaded, since it defeats running an isolated spec on its own. ## What verification cannot see Verification asks the class what it defines. Methods that exist only through `method_missing` on instances are invisible to `instance_double`, because a real instance would be needed to ask. The rspec-mocks documentation lists three ways out: define the methods explicitly (calling `super`), use `object_double` against a loaded instance, or register a `before_verifying_doubles` callback that asks the class to define its dynamic methods first. Verification also does not check **return values**: `and_return(42)` on a method that really returns a receipt object is accepted. ## Practical guidance - Default to `instance_double` for any collaborator that has a real class. - Keep plain `double` for ad-hoc values with no class behind them. - Make sure the full suite loads the doubled classes, or the String form verifies nothing. - Pair verifying doubles with at least one integration-level spec that exercises the real collaborator, since signatures are all a double can prove.

  • Why might instance_double("Notifications::SmsGateway") verify nothing when one spec file is run on its own?
    With a String argument, rspec-mocks verifies only if that constant is defined when the double is used. Run in isolation, the spec may never load the gateway's file, so the double silently acts like a plain `double`. Loading the class (a `require` in the spec or full-suite run) or setting `verify_doubled_constant_names = true` for full runs closes the gap.
  • A class defines its attribute readers through method_missing. What happens when you instance_double it and stub one of those readers?
    The stub fails as unimplemented, because `instance_double` asks the class which instance methods it defines, and `method_missing` methods are not among them. Workarounds from the rspec-mocks docs: define the methods explicitly and call `super`, use `object_double` on a loaded instance, or add a `before_verifying_doubles` callback that makes the class define them.

saying these in an interview costs you the question

  • instance_double also checks that and_return gives the right type
  • a plain double raises when you stub a method the real class lacks
  • instance_double with a String name always verifies, loaded or not
  • class_double verifies instance methods of the named class
  • instance_double makes the spec load the real class automatically