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 “partialdoublespy_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 “settingconstraintsspec.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 havereceived(:deliver).atleast(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
# received :deliver out of order