【问题标题】:Working around the need for partial mocks解决对部分模拟的需求
【发布时间】:2013-08-16 14:07:00
【问题描述】:

有时我会遇到想在测试中使用类方法的部分模拟的情况。目前,我正在使用不支持此功能的 minitest(可能是因为这首先不是一个好主意...)。

一个例子:

class ImportRunner

  def self.run *ids
    ids.each { |id| ItemImporter.new(id).import }
  end
end

class ItemImporter

  def initialize id
    @id = id
  end

  def import
    do_this
    do_that
  end

  private

    def do_this
      # do something with fetched_data
    end

    def do_that
      # do something with fetched_data
    end

    def fetched_data
      @fetched_data ||= DataFetcher.get @id
    end

end

我想单独测试ImportRunner.run 方法(主要是因为ItemImporter#import 很慢/很贵)。在 rspec 我会写一个这样的测试:

it 'should do an import for each id' do
  first_importer  = mock
  second_importer = mock

  ItemImporter.should_receive(:new).with(123).and_return(first_importer)
  first_importer.should_receive(:import).once
  ItemImporter.should_receive(:new).with(456).and_return(second_importer)
  second_importer.should_receive(:import).once

  ImportRunner.run 123, 456
end

问题的第一部分:是否可以在minitest中做类似的事情?


问题的第二部分:是对象协作的形式

collaborator = SomeCollaborator.new a_param
collaborator.do_work

糟糕的设计?如果是这样,你会如何改变它?

【问题讨论】:

  • 你经历过Minitest::Mock的东西吗?
  • 是的。 Minitest::Mock 提供完整的模拟对象以及方法的部分存根。但不是像我在这种情况下需要的那样部分嘲笑......

标签: ruby oop architecture minitest


【解决方案1】:

您所要求的在直接 Minitest 中几乎是可能的。 Minitest::Mock 不支持部分模拟,因此我们尝试通过存根 ItemImporter 的 new 方法并返回一个调用模拟的 lambda 来代替返回模拟。 (模拟中的模拟:Mockception)

def test_imports_for_each_id
  # Set up mock objects
  item_importer   = MiniTest::Mock.new
  first_importer  = MiniTest::Mock.new
  second_importer = MiniTest::Mock.new

  # Set up expectations of calls
  item_importer.expect :new, first_importer,  [123]
  item_importer.expect :new, second_importer, [456]
  first_importer.expect  :import, nil
  second_importer.expect :import, nil

  # Run the import
  ItemImporter.stub :new, lambda { |id| item_importer.new id } do
    ImportRunner.run 123, 456
  end

  # Verify expectations were met
  # item_importer.verify
  first_importer.verify
  second_importer.verify
end

除了调用item_importer.verify 之外,这将起作用。因为该模拟将返回其他模拟,所以验证满足所有期望的过程将在 first_importersecond_importer 模拟上调用其他方法,从而导致它们上升。因此,虽然您可以接近,但您无法准确复制您的 rspec 代码。为此,您必须使用支持部分模拟的不同模拟库,例如 RR

如果您觉得该代码很难看,请不要担心,确实如此。但这不是 Minitest 的错,而是测试中职责冲突的错。就像你说的,这可能不是一个好主意。我不知道这个测试应该证明什么。它看起来是在指定代码的实现,但它并没有真正传达预期的行为。这就是一些人所说的“过度嘲笑”。

Mocks 和 stub 在开发人员手中是重要且有用的工具,但很容易被带走。除了给人一种虚假的安全感之外,过度模拟的测试也可能是脆弱和嘈杂的。 - Rails AntiPatterns

我会重新考虑你想通过这个测试来完成什么。 Minitest 通过做出丑陋的东西应该看起来丑陋的设计选择来帮助您。

【讨论】:

  • 感谢您的回答!我不知道您可以将 proc 传递给 .stub ,每当调用存根方法时都会对其进行评估。这改变了一切;)我采用了您的实现并使其变得更加简单:我定义了一个映射importer_mapping = {123 => first_importer, 456 => second_importer},而不是item_importer,然后使用此过程调用.stubproc {|id| importer_mapping[id]}。这样,我不会在未验证的情况下使用模拟对象。对 .new 的正确调用通过正确的导入器接收 #import... 得到隐式验证
【解决方案2】:

您可以使用Mocha gem。我还在我的大部分测试中使用 MiniTest,并使用 Mocha 来模拟和存根方法。

【讨论】:

  • 感谢您的回答。我知道其他模拟框架(mocharr 和许多其他)。但我正在寻找仅使用 minitest... 的解决方案...或者是允许使用 minitest 进行测试而不必依赖部分模拟的重写
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-03
  • 1970-01-01
  • 1970-01-01
  • 2016-07-13
  • 2012-11-01
  • 1970-01-01
相关资源
最近更新 更多