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