【问题标题】:What's the "right" way to test functions that call methods on new instances?测试在新实例上调用方法的函数的“正确”方法是什么?
【发布时间】:2016-05-12 17:37:02
【问题描述】:

我似乎遇到过文献暗示使用 RSpec 的 any_instance_of 方法(例如 expect_any_instance_of)是不好的做法。 relish 文档甚至在“使用遗留代码”部分 (http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance) 下列出了这些方法,这意味着我不应该利用它编写新代码。

我觉得我经常编写依赖此功能的新规范。一个很好的例子是任何创建一个新实例然后调用它的方法的方法。 (在 MyModel 是 ActiveRecord 的 Rails 中)我经常编写方法来执行以下操作:

def my_method
    my_active_record_model = MyModel.create(my_param: my_val)
    my_active_record_model.do_something_productive
end

我通常会在编写规范时寻找使用expect_any_instance_of 调用的do_something_productive。例如:

expect_any_instance_of(MyModel).to receive(:do_something_productive)
subject.my_method

我能看到的唯一其他方法是使用一堆像这样的存根:

my_double = double('my_model')
expect(MyModel).to receive(:create).and_return(my_double)
expect(my_double).to receive(:do_something_productive)
subject.my_method

但是,我认为这更糟糕,因为 a) 它的写入时间更长且速度更慢,并且 b) 它比第一种方式更易碎和更白。为了说明第二点,如果我将my_method 更改为以下内容:

def my_method
    my_active_record_model = MyModel.new(my_param: my_val)
    my_active_record_model.save
    my_active_record_model.do_something_productive
end

然后规范的双重版本中断,但any_instance_of 版本工作正常。

所以我的问题是,其他开发人员是如何做到这一点的?我使用any_instance_of 的方法是否令人不悦?如果是这样,为什么?

【问题讨论】:

    标签: ruby-on-rails ruby unit-testing testing rspec


    【解决方案1】:

    这是一种咆哮,但这是我的想法:

    relish 文档甚至在“使用遗留代码”部分 (http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance) 下列出了这些方法,这意味着我不应该利用它编写新代码。

    我不同意这一点。模拟/存根在有效使用时是一种有价值的工具,应该与断言样式测试一起使用。这样做的原因是,模拟/存根启用了一种“由外而内”的测试方法,您可以在这种方法中最小化耦合并测试高级功能,而无需调用堆栈中的每个小数据库事务、API 调用或帮助方法。

    问题真的是你想测试状态还是行为?显然,您的应用程序涉及两者,因此将自己束缚在单一的测试范式中是没有意义的。通过断言/期望进行的传统测试对于测试状态是有效的,并且很少关心如何改变状态。另一方面,模拟迫使您考虑对象之间的接口和交互,因为您可以存根和填充返回值等,所以状态本身的突变负担更小。 但是,我会在使用*_any_instance_of 时敦促谨慎,并尽可能避免使用它。这是一个非常生硬的工具,很容易被滥用,尤其是在项目很小的时候,当项目更大时,它就会变成一种负担。我通常将*_any_instance_of 视为我的代码或测试可以改进的气味,但有时需要使用它。

    话虽如此,在您提出的两种方法之间,我更喜欢这种方法:

    my_double = double('my_model')
    expect(MyModel).to receive(:create).and_return(my_double)
    expect(my_double).to receive(:do_something_productive)
    subject.my_method
    

    它是显式的、隔离良好的,并且不会产生数据库调用的开销。如果my_method 的实现发生变化,它可能需要重写,但这没关系。由于它是完全隔离的,如果my_method 之外的任何代码发生更改,它可能不需要重写。将此与在数据库中删除一列可能会破坏几乎整个测试套件的断言进行对比。

    【讨论】:

      【解决方案2】:

      我没有比您提供的两个更好的解决方案来测试这样的代码。在存根/模拟解决方案中,我将使用allow 而不是expect 进行create 调用,因为create 调用不是规范的重点,但这是一个附带问题。我同意 stub 和 mocking 很痛苦,但我通常会这样做。

      但是,该代码只是有点功能嫉妒。将方法提取到 MyModel 上可以清除异味并消除测试问题:

      class MyModel < ActiveRecord::Base
        def self.create_productively(attrs)
          create(attrs).do_something_productive
        end
      end
      
      def my_method
        MyModel.create_productively(attrs)
      end
      
      # in the spec
      expect(MyModel).to receive(:create_productively)
      subject.my_method
      

      create_productively 是一种模型方法,因此可以而且应该使用真实实例对其进行测试,无需存根或模拟。

      我经常注意到需要使用 RSpec 中不太常用的功能意味着我的代码可以使用一些重构。

      【讨论】:

      • 您现在的create_productively 方法中没有完全相同的问题吗?
      • 如果主题除了在模型上创建和调用方法之外做更多的事情,那将更有帮助。但无论如何,请参阅我的更新。
      • 我的意思是,你现在如何测试create_productively。我根本看不出这有什么帮助。也许我只是想念它。
      • create_productively 完全处理它定义的模型,所以我认为在测试时不需要使用存根或模拟。 (与测试原始主题时相反,这是一个不同的模型;在一个模型上模拟方法而不是另一个模型的测试是有意义的。)我只需执行它并测试是否已创建适当的模型。如果您觉得测试 create_productively 仍然需要模拟,那您是对的,它不会回答您最初的问题。
      • 你现在如何测试 create_productively 应该对 create_productively 进行单独测试。
      【解决方案3】:
      def self.my_method(attrs)
        create(attrs).tap {|m| m.do_something_productive}
      end
      
      # Spec   
      let(:attrs) { # valid hash }
      
      describe "when calling my_method with valid attributes" do
        it "does something productive" do
          expect(MyModel.my_method(attrs)).to have_done_something_productive
        end
      end
      

      当然,您将对#do_something_productive 本身进行其他测试。

      权衡总是相同的:模拟和存根速度很快,但很脆弱。真实物体速度较慢但不那么脆弱,并且通常需要较少的测试维护。

      我倾向于为外部依赖项(例如 API 调用)或使用已定义但未实现的接口保留模拟/存根。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-06-13
        • 2017-11-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-23
        • 2011-09-08
        • 1970-01-01
        相关资源
        最近更新 更多