RSpec - 如何在辅助对象上正确使用双精度和存根方法?

时间:2017-03-30 10:00:47

标签: ruby-on-rails rspec double stub rspec-mocks

Rails控制器的一个动作创建一个辅助类的实例(比如SomeService),它执行一些工作并返回结果,类似于:

def create
  ...
  result = SomeService.new.process
  ...
end

我想要保留SomeService#process返回的内容。

我的问题是 - 我该怎么做?

以下作品:

allow_any_instance_of(SomeService).to receive(:process).and_return('what I want')

但是,rspec-mock文档不鼓励allow_any_instance_of使用reasons states here

  

rspec-mocks API是为单个对象实例设计的,但此功能适用于整个对象类。结果,存在一些语义上令人困惑的边缘情况。例如,在expect_any_instance_of(Widget).to receive(:name).twice中,它不清楚每个特定实例是否应该接收两次名称,或者是否预期两个接收总数。 (它是前者。)

     

使用此功能通常是一种设计气味。可能是您的测试试图做得太多或者被测对象太复杂了。

     

这是rspec-mocks最复杂的功能,并且历史上收到的bug报告最多。 (没有一个核心团队积极使用它,这没有帮助。)

我认为这个想法是做这样的事情:

some_service = instance_double('SomeService')
allow(some_service).to receive(:process).and_return('what I want')

但是,如何让控制器使用double而不是创建新实例 是SomeService

2 个答案:

答案 0 :(得分:2)

我通常会这样做。

let(:fake_service) { your double here or whatever }

before do
  allow(SomeService).to receive(:new).and_return(fake_service)
  # this might not be needed, depending on how you defined your `fake_service`
  allow(fake_service).to receive(:process).and_return(fake_results)
end

答案 1 :(得分:0)

我的建议是重塑与服务对象交互的方式:

class SomeService
  def self.call(*args)
    new(*args).tap(&:process)
  end

  def initialize(*args)
    # do stuff here
  end

  def process
    # do stuff here
  end

  def success?
    # optional method, might make sense according to your use case
  end
end

由于这是一个项目范围的约定,因此我们知道每个.call都会返回服务对象实例,我们将查询诸如#success?#error_messages等(主要取决于您的用例)。

在测试此类客户端时,我们仅应验证它们是否使用正确的参数调用类方法.call,就像模拟返回的值一样简单。

对此类方法的单元测试应证明它: -用适当的参数调用.new; -在创建的实例上调用#process; -返回创建的实例(不是过程的结果)。

将类方法作为服务对象接口的主要入口点有助于提高灵活性。 #initialize#process都可以设为私有,但我不希望出于测试目的。