Ruby 如何在RSpec中使用验证双精度设置负面消息预期?

Ruby 如何在RSpec中使用验证双精度设置负面消息预期?,ruby,rspec,Ruby,Rspec,我想在以下代码中测试对obj.my_method的条件调用: def method_to_test(obj) # ... obj.my_method if obj.respond_to?(:my_method) # ... end obj可以是Foo或Bar。一个实现了my_方法,另一个没有: class Foo def my_method; end end class Bar end 我的测试目前的结构如下: describe '#method_to_test' do

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

def method_to_test(obj)
  # ...
  obj.my_method if obj.respond_to?(:my_method)
  # ...
end
obj
可以是
Foo
Bar
。一个实现了
my_方法
,另一个没有:

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
def my_method
  raise # should never be executed in any case
end

def respond_to?(m_name)
  m_name != :my_method && super(name)
end
我完全理解RSpec提出这个错误的原因。尽管
obj
是一个双重验证码,我如何测试该行


PS:我已经启用了,所以将
实例双('Bar')
更改为
对象双('Bar.new)
或只是
Bar.new
没有帮助。

有趣而棘手的问题!从Rspec文档中,我发现:

[编辑:刚刚看到您已经理解了rspec投诉的原因,因此您可以跳转到可能的脏/不脏的解决方案部分。我将保留我在此处找到的解释,以防其他人遇到类似问题(请纠正我/添加更多相关信息!)]

验证双重实例不支持类报告为不存在的方法 因为需要根据类的实际实例进行验证。这是常见的 使用方法_缺失时的情况。 -

这些文档引用了instance_double的问题和method_missing的用法,但我相信这与您面临的问题是一样的。您的Bar类没有报告以响应:my_method。事实上,在失败的测试中,
method\u to\u test(obj)
从未被调用,因为当您尝试在实例上定义期望值时,测试失败:

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
当您尝试添加期望值时,Rspec会查找Bar类报告已定义的方法的列表,但没有找到
:my_方法
,并以预防性方式失败,认为它实际上是在帮助您识别测试中的问题(毕竟,您试图在不存在的东西上添加一个方法调用期望,所以这很可能是一个输入错误,对吧?——哎哟!)

可能的脏溶液

因此,恐怕没有一种一致的方法可以将此期望添加到您正在使用的实例中,而不必进行肮脏的欺骗。例如,您可以实际定义
:我的方法
在条形图上,这样您就可以添加消息期望,但是您必须在上下文中重新定义
响应?
对吧,结果是这样的:

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
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\u to\u 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_方法时引发的错误将不是类型为
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:
#方法对测试
当obj是一个酒吧
不调用my_方法(失败-1)
失败:
1) #方法_to _测试当obj为条形时,不调用我的方法
失败/错误:预期{method_to_test(obj)}。not_引发错误
不应出现异常,使用回溯获取#:
您可以使用
RSpec::Mocks::MockExpectationError
而不是
NoMethodError
来实现预期,但这也会创建脆弱的测试:如果您将测试条实例更改为一个真实的obj而不是一个双实例,那么如果调用了
my_方法,会引发
NoMethodError
?这项测试将继续通过,而不测试任何有价值的东西。此外(如评论中指出的),如果您这样做,RSPEC将警告您:

警告:使用期望{}。NoTyto to RaSeSeError(TraceReloReC类)会产生假阳性,因为字面上任何其他错误都会导致预期通过,包括露比提出的那些(例如NoththoDror,NameError和AgulSturror),这意味着您要测试的代码可能甚至没有达到。.not_引发_错误。可以通过设置:RSpec::expections.configuration.warn_about_potential_false_positives=false来抑制此消息。“

因此,在这些选项之间,我将指定异常的类型

可能的(最佳)解决方案

最后,我建议重构。使用
respond\u to?
通常表示您缺少一个适当的接口来跨这些多个类定义和实现。我无法帮助您了解重构的细节,因为这在很大程度上取决于您当前的代码上下文/实现细节。然而,您应该从成本角度评估它是否值得,如果不值得,我建议将
obj.my\u method if obj.respond\u to?(:my\u method)
提取到一个专用的方法调用中,这样断言它不会引发错误将是一个更可靠的断言,因为您可以在较少的上下文中引发奇怪的异常。此外,如果您这样做,您可以传递一个简单的
double
对象,该对象允许您按照最初的方式添加消息期望


很抱歉——冗长的回答:D

谢谢你详细的回答。关于“可能的肮脏解决方案”,这将是非常具体的实现。如果obj.is_a?(Fo