RSpec message expectations and verify-after-execution behavior
0 reputation · 20 Oct 2021, 13:52 UTC
0 reputation · 20 Oct 2021, 13:52 UTC
RSpec provides distinct mechanisms for verifying method calls on test doubles, specifically through traditional message expectations and the use of spies. While expect().to receive() sets a requirement before the action occurs, spies enable an "act first, assert later" pattern using have_received().
There is a design consideration regarding the internal state management when transitioning from a stubbed method to a verified spy. When a double is configured with allow().to receive(), it must track all subsequent calls to support post-execution verification without altering the object's primary behavior during the execution phase.
Does the have_received matcher rely on a global call history for all doubles, or is the tracking limited to objects explicitly configured with allow()? How does RSpec distinguish between a method that was never stubbed and one that was stubbed but never called during verification?
34775 reputation · 20 Oct 2021, 19:01 UTC
No. have_received does not use a global call history for all doubles. Tracking is per-object, on the RSpec-mocks proxy that is installed for a specific test double when you configure it with allow, expect, or create it as a spy. Verification after execution inspects the invocation record stored on that proxy, not a process-wide log.
allow(obj).to receive(:m) and expect(obj).to receive(:m) install a MessageExpectation on the proxy for that specific obj.have_received is a verify-after-execution matcher. It queries the invocation count stored on the MessageExpectation that was created by a prior allow or expect setup for that object/method.allow/expect or created as a spy. A real object with no mock proxy has no expectation registry, so have_received cannot be evaluated against it.MessageExpectation in the object's proxy.allow was called, an expectation exists with zero invocations, so have_received fails as a verification of received messages.have_received errors about the object not being a mock/spy or about no expectation being defined for that message, rather than silently returning false.double, instance_double, or an object you have explicitly stubbed with allow.allow(obj).to receive(:m).and_return(value). This creates the per-object expectation and enables call recording.obj.expect(obj).to have_received(:m). The matcher reads the recorded invocations from that object's proxy.Assumption: RSpec 3.x model with rspec-mocks. Behavior is version-sensitive; error wording and spy auto-recording semantics have changed across releases. Do not assume global history applies to non-mocked objects.
One diagnostic detail that changes the recommendation: are you calling have_received on a test double created by double/instance_double or on a real object with only allow applied? The proxy setup differs and determines whether verification is supported.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.