Choosing Verifying Doubles in RSpec to Catch Interface Drift Early
Learn why instance_double, class_double and object_double are safer than plain double(), how they validate method existence and signatures, and where their limits lie.
30 Aug 2026, 16:07 UTC

Why verifying doubles matter
When you stub a collaborator with double('name') or instance_double without a reference, RSpec accepts any method name you later stub. If the real class renames a method or changes its arity, the spec continues to pass, hiding a breaking change until integration tests or production fail. Verifying doubles (instance_double, class_double, object_double) bind the double to a real constant. Before allowing a stub, RSpec checks that the method exists on that constant and that the call signature matches the defined parameters. A mismatch produces a clear verification error, so interface drift is caught at test time.
How verifying doubles work
When RSpec creates a verifying double, it:
- Looks up the referenced constant (e.g.,
PaymentGateway). - Inspects the class’s public instance method table (or singleton methods for
class_double). - Compares each stubbed method name and the argument pattern supplied to
receive/withagainst the real method’s definition. - If the method is missing or the required arity/keyword names differ, RSpec raises a verification failure with a message like "
PaymentGateway#chargedoes not exist" or "wrong number of arguments (given 2, expected 1)". - If the constant cannot be loaded, RSpec aborts and asks you to either require the file or fall back to an unverified double.
This mechanism only validates the shape of the interface—method existence and signature (required positional arguments and keyword names). It does not check argument types, return values, or private/protected methods.
Worked example
Assume a simple payment gateway class:
# payment_gateway.rb
class PaymentGateway
def charge(amount_cents, currency: 'USD')
# real implementation omitted
end
end
A spec that uses an verifying double:
# spec/payment_gateway_spec.rb
require 'payment_gateway'
RSpec.describe 'Order processing' do
it 'charges the gateway' do
gateway = instance_double(PaymentGateway)
# This stub matches the real method’s name and keyword argument
allow(gateway).to receive(:charge).with(1_000, currency: 'EUR').and_return(true)
order = Order.new(gateway: gateway)
order.process
expect(gateway).to have_received(:charge).with(1_000, currency: 'EUR')
end
end
If someone renames the method in payment_gateway.rb to debit and runs the spec, the output will be:
Failure/Error: allow(gateway).to receive(:charge).with(1_000, currency: 'EUR').and_return(true)
# does not implement: charge
# Verified double error: instance_double(PaymentGateway) received unexpected message :charge
The same spec with a plain double('gateway') would continue to pass, silently masking the drift.
Configuration: extending verification to partial doubles
To apply the same interface checks when you stub methods directly on real objects, enable:
# spec/spec_helper.rb
RSpec.configure do |config|
config.verify_partial_doubles = true
end
With this setting, allow(real_object).to receive(:unknown_method) will fail if unknown_method is not present on the object's class, protecting against ad‑hoc stubs that diverge from the real API.
Limits of verifying doubles
- Signature only: Keyword names and required positional arity are checked; optional arguments, default values, and argument types are not validated.
- Public interface: Private or protected methods are ignored; attempting to stub them triggers a verification failure.
- Dynamic methods: Methods defined at runtime (e.g., via
method_missingordefine_method) cannot be verified because they are not in the method table at load time. - Load cost: The referenced class must be loadable, which pulls in its dependencies and can increase spec boot time.
You can verify these limits in your own project:
- Try stubbing a private method on an
instance_double; you should see a verification error. - Define a method with
define_methodafter the class loads and stub it; the verification will pass because the method is not in the static method table. - Check that changing only the type of an argument (e.g., expecting a
Stringbut passing anInteger) does not cause a verification failure.
Common mistakes and how to avoid them
- Stubbing nonexistent methods on plain doubles: This hides interface drift. Replace
doublewith a verifying double whenever the class is available. - Assuming
with(...)validates argument types: It only checks equality of the values you provide; use shared examples or contract tests for type safety. - Using
as_null_objector falling back todoubleto silence verification errors: This removes the safety net. Fix the stub to match the real interface instead. - Stubbing private methods: Verifying doubles will reject them; if you truly need to test private behavior, consider refactoring to make it public or testing through the public interface.
When to fall back to plain doubles
Verifying doubles are ideal for stable collaborators whose definitions are loaded in the test environment. Use plain doubles only when:
- The collaborator does not exist yet (e.g., you are driving an interface from the outside).
- The class cannot be loaded without heavy side effects, and you accept the risk of interface drift.
- You are writing a spike or exploratory test where speed outweighs safety.
- Pin your versions in the Gemfile, e.g.,
gem 'rspec-mocks', '~> 3.12.0'andgem 'rspec-core', '~> 3.12.0'(these correspond to Ruby 3.2.x). - Run
bundle info rspec-mocksto confirm the installed version. - Create the example class and spec shown above.
- Execute
bundle exec rspec spec/payment_gateway_spec.rband observe a passing test. - Rename
chargetodebitin the class and rerun; you should see a verification failure. - Toggle
config.verify_partial_doubles = trueand stub an unknown method on a real object to confirm the same failure mode.
In those cases, comment the reason for using an unverified double so future maintainers know the trade‑off.
Practical verification steps
If the errors differ from those described, consult the changelog for your specific rspec-mocks/Ruby pair, as keyword‑argument handling and error messages have evolved across releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.