如何在RSpec中使用验证双重设置否定消息期望?

时间:2016-01-21 09:43:31

标签: ruby rspec

我想在以下代码中测试对obj.my_method的条件调用:

def method_to_test(obj)
  # ...
  obj.my_method if obj.respond_to?(:my_method)
  # ...
end

obj可以是FooBar。一个实现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无效。

1 个答案:

答案 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