Home > Enterprise >  Is there a way in RSpec to assert both number of calls and the list of arguments together?
Is there a way in RSpec to assert both number of calls and the list of arguments together?

Time:02-04

I want to assert that a certain method is called exactly N times (no more, no less) with specific arguments with a specific order. Also I don't want to actually execute this method so first I stub it with allow().

Suppose I have this code:

class Foo
  def self.hello_three_times
    Foo.hello(1)
    Foo.hello(2)
    Foo.hello(3)
  end

  def self.hello(arg)
    puts "hello #{arg}"
  end
end

I want to test method hello_three_times that it actually calls hello three times with 1, 2, and 3 as arguments. (And I don't want to really call hello in tests because in reality it contains side effects and is slow.)

So, if I write this test

RSpec.describe Foo do
  subject { Foo.hello_three_times }

  it do
    allow(Foo).to receive(:hello).and_return(true)
    expect(Foo).to receive(:hello).with(1).once.ordered
    expect(Foo).to receive(:hello).with(2).once.ordered
    expect(Foo).to receive(:hello).with(3).once.ordered
    subject
  end
end

it passes but it doesn't guarantee there are no additional calls afterwards. For example, if there is a bug and method hello_three_times actually looks like this

def self.hello_three_times
  Foo.hello(1)
  Foo.hello(2)
  Foo.hello(3)
  Foo.hello(4)
end

the test would still be green.

If I try to combine it with exactly(3).times like this

RSpec.describe Foo do
  subject { Foo.hello_three_times }

  it do
    allow(Foo).to receive(:hello).and_return(true)
    expect(Foo).to receive(:hello).exactly(3).times
    expect(Foo).to receive(:hello).with(1).once.ordered
    expect(Foo).to receive(:hello).with(2).once.ordered
    expect(Foo).to receive(:hello).with(3).once.ordered
    subject
  end
end

it fails because RSpec seems to be treating the calls as fulfilled after the first expect (probably in this case it works in such a way that it expects to have 3 calls first, and then 3 more calls individually, so 6 calls in total):

Failures:
  1) Foo is expected to receive hello(3) 1 time
     Failure/Error: expect(Foo).to receive(:hello).with(1).once.ordered
       (Foo (class)).hello(1)
           expected: 1 time with arguments: (1)
           received: 0 times

Is there a way to combine such expectations so that it guarantees there are exactly 3 calls (no more, no less) with arguments being 1, 2, and 3 (ordered)?

CodePudding user response:

Oh, I think I found the solution. I can use a block for that:

RSpec.describe Foo do
  subject { Foo.hello_three_times }

  let(:expected_arguments) do
    [1, 2, 3]
  end

  it do
    allow(Foo).to receive(:hello).and_return(true)
    call_index = 0
    expect(Foo).to receive(:hello).exactly(3).times do |argument|
      expect(argument).to eq expected_arguments[call_index]
      call_index  = 1
    end
    subject
  end
end

It gets the job done guaranteeing there are exactly 3 calls with correct arguments.

It doesn't look very pretty though (introducing that local variable call_index, ugh). Maybe there are prettier solutions out of the box?

CodePudding user response:

You can loop through the expected values and call the expectation for each one like this. You can do the same thing for the allow to ensure that it is only being called with the args that you want, or just keep the allow as you had it to allow anything.

RSpec.describe Foo do
  subject { Foo.hello_three_times }
  let(:expected_args){ [1, 2, 3] }

  before do
    expected_args.each do |arg|
      allow(Foo).to receive(:hello).with(arg).and_return(true)
    end
  end

  it 'calls the method the expected times' do
    expected_args.each do |arg|
      expect(Foo).to receive(:hello).with(arg).once
      subject
    end
  end
end
  • Related