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

RSpec - How to properly use doubles and stub methods on helper objects?

Rails 控制器的一个动作生成一个助手实例 class(比如 SomeService),它执行一些工作并 returns 一个结果,一些东西行:

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

我要存根SomeService#processreturns.

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

以下作品:

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

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

The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether each specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)

Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.

It is the most complicated feature of rspec-mocks, and has historically received the most bug reports. (None of the core team actively use it, which doesn't help.)

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

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

但是,如何让控制器使用双精度而不创建新实例 SomeService?

我经常这样做。

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

我的建议是改造您与服务对象的交互方式:

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 returns 服务对象实例,我们查询 #success?#error_messages 等内容等(很大程度上取决于您的用例)。

在测试此 class 的客户端时,我们应该只验证它们使用正确的参数调用 class 方法 .call,这与模拟返回值一样简单。

此 class 方法的单元测试应证明这一点: - 使用适当的参数调用 .new; - 在创建的实例上调用 #process; - returns 创建的实例(不是过程的结果)。

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