如何在RSpec中同时断言方法调用次数与指定有序参数?
问题描述
需要断言某个方法被精确调用N次(不多不少),且调用参数符合指定顺序;同时要stub该方法,避免执行其实际逻辑(比如有副作用或执行缓慢)。
示例代码:
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
测试需求:验证hello_three_times按顺序调用hello三次,参数分别为1、2、3,且没有额外调用。
现有测试的问题:
- 第一种测试仅验证了指定参数的调用顺序,但无法阻止额外调用(比如代码中多了
Foo.hello(4)时测试仍通过):
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
- 第二种测试尝试结合
exactly(3).times,但因RSpec的匹配器逻辑冲突导致失败(错误认为需要总共6次调用):
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
错误信息:
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
解决方案
可以通过两种方式实现需求:
方式一:结合exactly与block验证参数顺序
利用block捕获每次调用的参数,按顺序验证,同时通过exactly(3).times确保调用次数精确:
RSpec.describe Foo do subject { Foo.hello_three_times } it "calls hello exactly 3 times in order with arguments 1, 2, 3" do expected_args = [1, 2, 3] expect(Foo).to receive(:hello).exactly(3).times do |arg| expect(arg).to eq(expected_args.shift) end subject end end
这种方式既保证了调用次数恰好3次,又通过block按顺序验证每次的参数,同时receive本身已经stub了原方法,不需要额外的allow。
方式二:链式期望+禁止额外调用
先按顺序验证指定参数的调用,再通过not_to receive确保没有后续的额外调用:
RSpec.describe Foo do subject { Foo.hello_three_times } it "calls hello exactly 3 times in order with arguments 1, 2, 3" do expect(Foo).to receive(:hello).with(1).ordered expect(Foo).to receive(:hello).with(2).ordered expect(Foo).to receive(:hello).with(3).ordered expect(Foo).not_to receive(:hello) subject end end
这里的not_to receive(:hello)会确保在完成前三次指定调用后,没有任何额外的hello调用。同样,expect(...).to receive已经替代了原方法的执行,无需额外allow。
关键说明
- 不需要单独使用
allow:expect(...).to receive(...)会自动stub目标方法,阻止原逻辑执行,同时记录调用情况用于断言。 - 避免重复期望冲突:不要同时写全局的
exactly(N).times和单独的参数期望,因为RSpec会将它们视为独立的断言,导致期望调用次数叠加。
内容的提问来源于stack exchange,提问作者Dmytro Savochkin
相关产品推荐
相关产品推荐

