Ruby 3.x中使用RSpec测试方法调用未包含特定关键字参数的可行方案(不修改原方法)
我明白你遇到的困扰了——在Ruby 2.x里用hash_excluding(:branch)能完美验证clone方法未传入branch关键字参数,但升级到Ruby 3.x后这个写法直接失效了。这本质上是Ruby 3对关键字参数和普通哈希参数做了严格区分导致的,RSpec旧的哈希匹配器已经没法识别关键字参数了。
先帮你理清楚问题根源:你定义的clone方法用的是branch: nil这种原生关键字参数,而Ruby 3之前的版本里关键字参数和哈希是模糊处理的,所以hash_excluding能凑效;但Ruby 3把两者彻底分开后,hash_excluding只能匹配传入的哈希对象,自然匹配不上无参数或者关键字参数的调用,就会出现你看到的「expected hash_not_including,got no args」错误。
你后来尝试的expect(repository).not_to receive(:clone).with(branch: anything)确实存在漏洞:这个测试的逻辑是「不要接收到仅传入branch参数的调用」,但如果调用时同时传了branch和depth,with(branch: anything)这个匹配条件就不成立了(因为实际参数是两个关键字),所以not_to会错误地认为符合预期,导致测试误通过,这显然不是你想要的结果。
下面给你两个不需要修改原方法的可行方案:
方案1:用Block自定义参数检查(兼容所有RSpec版本)
这是最稳妥的通用方案,不管你的RSpec版本新旧都能正常工作。我们可以在receive后面追加一个block,捕获所有传入的关键字参数,然后直接检查参数中是否包含:branch:
it 'clones the created repository without a branch' do expect(repository).to receive(:clone) do |**kwargs| expect(kwargs).not_to have_key(:branch) end method_call end
这里的**kwargs会捕获所有传入的关键字参数(如果有的话),然后我们断言这个关键字参数哈希里不包含:branch——不管有没有传depth或其他参数,只要没传branch就会通过测试,完全符合你的需求。
方案2:用RSpec官方的keywords_excluding匹配器(需RSpec 3.12+)
如果你已经把RSpec升级到3.12或更新的版本,其实官方已经添加了专门针对关键字参数的匹配器keywords_excluding,用法和你原来的hash_excluding几乎一致,直接用就行:
it 'clones the created repository without a branch' do expect(repository).to receive(:clone).with(keywords_excluding(:branch)) method_call end
这个匹配器是专门为Ruby 3的关键字参数规则设计的,代码更简洁优雅,也更符合RSpec的惯用写法。
这两种方案都不需要改动你的业务代码,完全满足「只调整测试逻辑」的要求。如果你的项目还在用较旧的RSpec版本,优先选方案1;如果已经升级到新版本,方案2是更优的选择。
备注:内容来源于stack exchange,提问作者Sergio

