【问题标题】:RSpec - How to properly use doubles and stub methods on helper objects?RSpec - 如何在辅助对象上正确使用双精度和存根方法?
【发布时间】:2017-03-30 10:00:47
【问题描述】:

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 中,不清楚每个特定实例是否应该接收 name 两次,或者是否预期两个接收总数。 (是前者。)

使用此功能通常带有设计味道。可能是您的测试试图做的太多,或者被测对象太复杂。

它是 rspec-mocks 中最复杂的功能,历史上收到的错误报告最多。 (核心团队没有人积极使用它,这无济于事。)

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

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

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

【问题讨论】:

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


    【解决方案1】:

    我通常会这样做。

    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
    

    【讨论】:

    • 如果SomeService类只调用一个类方法怎么办。类似SomeService.some_class_method,我们需要测试some_class_method 是否被执行?
    • @MatayoshiMariano:是的,怎么样?
    • @SergioTulentsev 例如,我如何测试new 方法是否在SomeService 上被调用?我可以实现的唯一方法是在返回SomeService 的控制器类中创建一个方法some_service_class,然后对该方法进行存根以返回间谍allow_any_instance_of(SomeController).to(receive(:some_service_class).and_return(service_class_spy)),然后在测试expect(service_class_spy).to have_received(:new) 中。我不喜欢这种方式,我认为那是不干净的。有没有更好的办法? PS:问我什么,可能我不清楚。
    • @MatayoshiMariano 只是 expect(SomeService).to receive(:some_class_method)?不需要间谍什么的
    • @SergioTulentsev 出于某种原因,这对我不起作用。要添加更多信息,SomeService 类是来自 Shoryuken 的工作人员
    【解决方案2】:

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

    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 都可以设为私有,但我不希望用于测试目的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-19
      • 1970-01-01
      • 2015-11-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多