2

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)?

  • Does this answer your question? [RSpec: specifying multiple calls to a method with different argument each time](https://stackoverflow.com/questions/1971729/rspec-specifying-multiple-calls-to-a-method-with-different-argument-each-time) – anothermh Feb 02 '23 at 16:14
  • Nope, I don't think it does. It doesn't help with guaranteeing there are no extra calls afterwards. I've edited my question to add more details/context. – Dmytro Savochkin Feb 02 '23 at 19:31

2 Answers2

2

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?

1

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
akerr
  • 355
  • 1
  • 11
  • This probably doesn't enforce order and it would allow more than 3 calls. – Sergio Tulentsev Feb 03 '23 at 17:31
  • Actually I think this is good! I upvoted this, thank you! I forgot I could achieve my requirements by doing individual `allow` calls for each of the arguments separately. It really doesn't enforce order right now as @SergioTulentsev pointed out but it can be fixed easily by adding `.ordered` to each `expect`. And it actually doesn't allow more than 3 calls so it's good in this regard (it was allowing more than 3 calls for my code because I used `allow` for any argument). – Dmytro Savochkin Feb 03 '23 at 21:00