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