Verified doubles in RSpec: choosing between allow, expect, and spies without brittle mocks
0 reputation · 04 Feb 2025, 12:02 UTC
0 reputation · 04 Feb 2025, 12:02 UTC
Our team is standardizing how we isolate collaborators in unit tests, and we keep hitting the same unresolved decision: when to use allow, when to use expect, and when spies are the better fit. The suite mixes all three styles inconsistently, and reviewers disagree on what each test is actually promising.
We also want to rely on verified doubles (instance_double) so that stubbed methods that no longer exist on the real class fail loudly instead of silently passing. What is unclear is how strict that verification is in practice — for example, whether it also checks argument arity, and what happens when the doubled class is not loaded in the test environment.
Assume a current RSpec 3.x setup with verify_partial_doubles enabled. We have not yet measured how much of the existing suite would break under a stricter policy.
Specifically:
expect(...).to receive) is preferable to a plain stub (allow)?instance_double validate method arguments as well as method names, and does it require the real class to be loadable?double usage to verified doubles wholesale?29775 reputation · 04 Feb 2025, 16:37 UTC
To reduce brittle mocks and standardize a growing suite, the distinction between allow, expect, and spies should be based on whether the interaction is a requirement for execution or the primary behavior being verified.
allow (Stubbing): When a collaborator must return a specific value for the system under test (SUT) to function, but the fact that the method was called is not the goal of the test. This prevents over-specification.expect(...).to receive (Mocking): When the call to the collaborator is the primary outcome. Use this for side-effect-heavy operations (e.g., sending an email, triggering a payment gateway) where a missing call constitutes a failure.instance_doubleVerified doubles solve the "silent failure" problem by checking the stubbed interface against the actual class definition.
| Feature | Behavior |
|---|---|
| Method Existence | Fails if the method does not exist on the real class. |
| Argument Arity | Fails if the number of arguments passed to the stub differs from the real method signature. |
| Class Loading | The real class must be loaded in the Ruby environment; otherwise, RSpec cannot verify the interface. |
Converting generic double usage to instance_double across a large suite may cause a spike in failures due to:
To verify that your instance_double is working, create a test with a non-existent method and run:
# This should raise a RSpec::Mocks::MockExpectationError
instance_double("User", non_existent_method: "value")
Diagnostic Detail Needed: Are you utilizing any dynamic method generation (e.g., method_missing or define_method) in your collaborators? Verified doubles cannot validate methods created dynamically at runtime.
Use comments to ask for clarification. Post a solution as an answer.
2,860 reputation · 04 Feb 2025, 22:21 UTC
When you use instance_double (or class_double) with allow, expect, or a spy, RSpec checks not only that the method exists but also that the number and kind of arguments match the real method’s signature. This includes positional arguments and keyword arguments: if the real method expects def foo(bar:, baz:), stubbing it with allow(foo).to receive(:foo).with(bar: 1, qux: 2) will raise an ArgumentError because :qux is not a valid keyword. Default values are not validated, so omitting an optional keyword is allowed.
You can verify this behavior by intentionally mismatching keywords in a spec and observing the verification error before the test runs.