Spies
Message expectations put an example's expectation at the start, before you've invoked the
code-under-test. Many developers prefer using an arrange-act-assert (or given-when-then)
pattern for structuring tests. Spies are an alternate type of test double that support this
pattern by allowing you to expect that a message has been received after the fact, using
have_received
.
You can use any test double (or partial double) as a spy, but the double must be setup to spy on the messages you care about. Spies automatically spy on all messages, or you can allow a message to spy on it.
have_received
supports the same fluent interface for setting constraints that normal message expectations do.
Note: The have_received
API shown here will only work if you are using rspec-expectations.
Note: have_received(...).with(...)
is unable to work properly when passed arguments are mutated after the spy records the received message.
Using a spy
Given a file named "spy_spec.rb" with:
RSpec.describe "have_received" do
it "passes when the message has been received" do
invitation = spy('invitation')
invitation.deliver
expect(invitation).to have_received(:deliver)
end
end
When I run rspec spy_spec.rb
Then the examples should all pass.
Spy on a method on a partial double
Given a file named "partial_double_spy_spec.rb" with:
class Invitation
def self.deliver; end
end
RSpec.describe "have_received" do
it "passes when the expectation is met" do
allow(Invitation).to receive(:deliver)
Invitation.deliver
expect(Invitation).to have_received(:deliver)
end
end
When I run rspec partial_double_spy_spec.rb
Then the examples should all pass.
Failure when the message has not been received
Given a file named "failure_spec.rb" with:
class Invitation
def self.deliver; end
end
RSpec.describe "failure when the message has not been received" do
example "for a spy" do
invitation = spy('invitation')
expect(invitation).to have_received(:deliver)
end
example "for a partial double" do
allow(Invitation).to receive(:deliver)
expect(Invitation).to have_received(:deliver)
end
end
When I run rspec failure_spec.rb --order defined
Then it should fail with:
1) failure when the message has not been received for a spy
Failure/Error: expect(invitation).to have_received(:deliver)
(Double "invitation").deliver(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
And it should fail with:
2) failure when the message has not been received for a partial double
Failure/Error: expect(Invitation).to have_received(:deliver)
(Invitation (class)).deliver(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Set constraints using the fluent interface
Given a file named "setting_constraints_spec.rb" with:
RSpec.describe "An invitation" do
let(:invitation) { spy("invitation") }
before do
invitation.deliver("foo@example.com")
invitation.deliver("bar@example.com")
end
it "passes when a count constraint is satisfied" do
expect(invitation).to have_received(:deliver).twice
end
it "passes when an order constraint is satisfied" do
expect(invitation).to have_received(:deliver).with("foo@example.com").ordered
expect(invitation).to have_received(:deliver).with("bar@example.com").ordered
end
it "fails when a count constraint is not satisfied" do
expect(invitation).to have_received(:deliver).at_least(3).times
end
it "fails when an order constraint is not satisfied" do
expect(invitation).to have_received(:deliver).with("bar@example.com").ordered
expect(invitation).to have_received(:deliver).with("foo@example.com").ordered
end
end
When I run rspec setting_constraints_spec.rb --order defined
Then it should fail with the following output:
| 4 examples, 2 failures |
| |
| 1) An invitation fails when a count constraint is not satisfied |
| Failure/Error: expect(invitation).to have_received(:deliver).at_least(3).times |
| (Double "invitation").deliver(*(any args)) |
| expected: at least 3 times with any arguments |
| received: 2 times with any arguments |
| |
| 2) An invitation fails when an order constraint is not satisfied |
| Failure/Error: expect(invitation).to have_received(:deliver).with("foo@example.com").ordered |
| #<Double "invitation"> received :deliver out of order |