我想在以下代码中测试对obj.my_method
的条件调用:
def method_to_test(obj)
# ...
obj.my_method if obj.respond_to?(:my_method)
# ...
end
obj
可以是Foo
或Bar
。一个实现my_method
,另一个不实现:
class Foo
def my_method; end
end
class Bar
end
我的测试目前的结构如下:
describe '#method_to_test' do
context 'when obj is a Foo' do
let(:obj) { instance_double('Foo') }
it 'calls my_method' do
expect(obj).to receive(:my_method)
method_to_test(obj)
end
end
context 'when obj is a Bar' do
let(:obj) { instance_double('Bar') }
it 'does not call my_method' do
expect(obj).not_to receive(:my_method) # <- raises an error
method_to_test(obj)
end
end
end
第一个示例通过,但第二个示例在设置否定消息期望时引发错误,因为obj
是未实现my_method
的验证双重:
#method_to_test
when obj is a Foo
calls my_method
when obj is a Bar
does not call my_method (FAILED - 1)
Failures:
1) #method_to_test when obj is a Bar does not call my_method
Failure/Error: expect(obj).not_to receive(:my_method)
the Bar class does not implement the instance method: my_method
我完全理解为什么RSpec会引发这个错误。尽管obj
是验证双倍,我该如何测试线?
PS:我已启用verify_partial_doubles
,因此将instance_double('Bar')
更改为object_double(Bar.new)
或仅Bar.new
无效。
答案 0 :(得分:1)
有趣且棘手的问题!从Rspec文档中,我发现了这个:
[编辑:刚看到你已经明白了为什么rspec会抱怨,所以你可以跳到可能脏/少脏的解决方案部分。我会保留我在此处找到的解释,以防其他人遇到类似问题(请纠正我/添加更多信息!)
验证实例双精灵不支持该类报告不存在的方法 因为该类的实际实例需要进行验证。这通常是 使用method_missing时的情况。 - https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/dynamic-classes
文档引用了instance_double的问题和method_missing的用法,但我相信它与您面临的问题相同。您的Bar类没有报告以回复:my_method 。实际上,在失败的测试中,method_to_test(obj)
永远不会被调用,因为当您尝试在instance_double 上定义期望时测试失败:
describe '#method_to_test' do
context 'when obj is a Bar' do
let(:obj) { instance_double('Bar') }
it 'does not call my_method' do
puts "A"
expect(obj).not_to receive(:my_method) # <- raises an error
puts "B" # <- never runs
method_to_test(obj)
end
end
end
产生
#method_to_test
when obj is a Bar
A
does not call my_method (FAILED - 1)
当您尝试添加期望时,Rspec将查找Bar类报告已定义的方法列表,但未找到:my_method
,并且预防性地失败,正在考虑&# 39; s实际上帮助你确定测试中的一个问题(毕竟你正试图在一个不存在的东西上添加一个方法调用期望,所以它很可能是一个错字,对吧? - 哎哟! )。
可能的肮脏解决方案
所以,我担心没有一致的方法可以将这个期望添加到你没有肮脏欺骗的instance_double中。例如,你可以在Bar 上实际定义:my_method
,这样你就可以添加消息期望,但是你必须在Bar的上下文中重新定义respond_to?
,结果是这样的:
def my_method
raise # should never be executed in any case
end
def respond_to?(m_name)
m_name != :my_method && super(name)
end
但你为什么要那样做呢?这很脏,反直觉!这种策略可以适用于我们所讨论的非常具体的案例,但这会将您的测试与您的具体实施结合起来,并且将其作为文档的价值无效。
可能不那么肮脏的解决方案
所以,我的建议是不依赖于方法期望,而是依赖于期望的行为。你想要的是method_to_test(obj)
的执行不会因为Bar实例上缺少方法而引发异常。你可以这样做:
describe '#method_to_test' do
context 'when obj is a Bar' do
let(:obj) { instance_double('Bar') }
it 'does not call my_method' do
expect { method_to_test(obj) }.not_to raise_error
end
end
end
缺点是,如果在执行方法体时出现任何异常,则此测试将失败,而理想情况下,我们希望仅在NoMethodError
被引发时才会失败 。你可以试试这个,但它不会起作用:
it 'does not call my_method' do
expect { method_to_test(obj) }.not_to raise_error(NoMethodError)
end
原因是在obj上调用:my_method
的情况下引发的错误不是NoMethodError
类型,而是RSpec::Mocks::MockExpectationError
类型:
#method_to_test
when obj is a Bar
does not call my_method (FAILED - 1)
Failures:
1) #method_to_test when obj is a Bar does not call my_method
Failure/Error: expect { method_to_test(obj) }.not_to raise_error
expected no Exception, got #<RSpec::Mocks::MockExpectationError: #<InstanceDouble(Bar) (anonymous)> received unexpected message :my_method with (no args)> with backtrace:
您可以使用RSpec::Mocks::MockExpectationError
而不是NoMethodError
来表达期望,但这也会产生脆弱的测试:如果您将测试条实例更改为真正的obj而不是instance_double会导致什么NoMethodError
如果调用了:my_method
?这个测试会继续通过而不测试任何值得的东西。另外(正如评论中所指出的),如果你这样做,RSPEC会警告你:
&#34;警告:使用expect {}。not_to raise_error(SpecificErrorClass)会产生误报,因为任何其他错误都会导致期望通过,包括Ruby引发的错误(例如NoMethodError,NameError和ArgumentError),意思是您打算测试的代码甚至可能无法访问。而是考虑使用expect {}。not_to raise_error。可以通过设置来禁止此消息:RSpec :: Expectations.configuration.warn_about_potential_false_positives = false。&#34;
因此,在这些选项之间,我不指定例外类型。
可能(最佳)解决方案
最后,我建议重构。使用respond_to?
通常表明您缺少在这些多个类中定义和实现的适当接口。我无法帮助解决这种重构的细节,因为这将高度依赖于您当前的代码上下文/实现细节。但是你应该评估它是否值得花费成本,如果不是,我建议将obj.my_method if obj.respond_to?(:my_method)
提取到一个专用的方法调用中,这样断言它不会raise_error将是一个更可靠的断言,给你& #39; d具有较少的上下文,可以引发奇怪的异常。此外,如果您这样做,您可以传递一个简单的double
对象,该对象允许您按照最初的预期方式添加消息期望。
对不起--verbose回答:D