【问题标题】:Stubbing out ActiveRecord models in Service tests在服务测试中剔除 ActiveRecord 模型
【发布时间】:2015-05-07 14:13:34
【问题描述】:

我遵循 TDD 方法来构建我们的应用程序,并创建一大堆服务对象,严格保持模型用于数据管理。

我构建的许多服务都与模型交互。以 MakePrintsForRunner 为例:

class MakePrintsForRunner

  def initialize(runner)
    @runner = runner
  end

  def from_run_report(run_report)
    run_report.photos.each do |photo|
      Print.create(photo: photo, subject: @runner)
    end
  end

end

我很欣赏 create 方法可以被抽象为 Print 模型,但现在让我们保持原样。

现在,我希望在 MakePrintsForRunner 规范中避免包含 spec_helper,因为我希望我的服务规范非常快。

相反,我像这样删除 Print 类:

describe RunnerPhotos do

  let(:runner) { double }
  let(:photo_1) { double(id: 1) }
  let(:photo_2) { double(id: 2) }
  let(:run_report) { double(photos: [photo_1, photo_2]) }

  before(:each) do
    @service = RunnerPhotos.new(runner)
  end

  describe "#create_print_from_run_report(run_report)" do

    before(:each) do
      class Print; end
      allow(Print).to receive(:create)
      @service.create_print_from_run_report(run_report)
    end

    it "creates a print for every run report photo associating it with the runners" do
      expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
      expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
    end
  end

end

一切都变绿了。完美!

... 没那么快。当我运行整个测试套件时,根据种子顺序,我现在遇到了问题。

似乎class Print; end 行有时会覆盖print.rb 的 Print 定义(显然继承自 ActiveRecord),因此在套件中的不同点上的一系列测试失败。一个例子是:

NoMethodError:
  undefined method 'reflect_on_association' for Print:Class

这导致了一个不愉快的套件。

关于如何解决这个问题的任何建议。虽然这是一个例子,但有很多次服务直接引用模型的方法,我采用上述方法将它们排除在外。有没有更好的办法?

【问题讨论】:

    标签: ruby-on-rails ruby tdd


    【解决方案1】:

    您不必创建Print 类,只需使用已加载的类并将其存根:

    describe RunnerPhotos do
    
      let(:runner) { double }
      let(:photo_1) { double(id: 1) }
      let(:photo_2) { double(id: 2) }
      let(:run_report) { double(photos: [photo_1, photo_2]) }
    
      before(:each) do
        @service = RunnerPhotos.new(runner)
      end
    
      describe "#create_print_from_run_report(run_report)" do
    
        before(:each) do
          allow(Print).to receive(:create)
          @service.create_print_from_run_report(run_report)
        end
    
        it "creates a print for every run report photo associating it with the runners" do
          expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
          expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
        end
      end
    
    end
    

    编辑

    如果你真的需要单独创建这个测试范围内的类,你可以在测试结束时取消定义它(来自How to undefine class in Ruby?):

    before(:all) do
      unless Object.constants.include?(:Print)
        class TempPrint; end
        Print = TempPrint
      end
    end
    
    after(:all) do
      if Object.constants.include?(:TempPrint)
        Object.send(:remove_const, :Print)
      end
    end
    

    【讨论】:

    • Print 类没有加载,但是。我明确试图避免包含 print.rb,因为它依赖于 ActiveRecord,因此会使规范测试变慢。运行您的代码会导致:NameError: uninitialized constant Print
    • 是从您的代码中调用的reflect_on_association 方法吗?
    • 不,它将在完整测试套件的其他地方的 Print 中调用,因为它只是一个 ActiveRecord::Reflection 方法
    • 啊,有趣,我从来没有见过那种删除东西的方法。可悲的是,现在测试套件导致 `RuntimeError: Circular dependency detected while autoloading constant Print
    • 如果你让类创建有条件(unless Object.constants.include?(:Print))?
    【解决方案2】:

    我很欣赏 create 方法可以被抽象为 Print 模型,但现在让我们保持原样。

    让我们看看如果我们忽略这一行会发生什么。

    你难以为一个类打桩是设计不灵活的标志。考虑将已经实例化的对象传递给 MakePrintsForRunner 的构造函数或方法 #from_run_report。选择哪个取决于对象的持久性 - 打印配置是否需要在运行时更改?如果不是,则传递给构造函数,如果是,则传递给方法。

    所以对于我们的第 1 步:

    class MakePrintsForRunner
      def initialize(runner, printer)
        @runner = runner
        @printer = printer
      end
    
      def from_run_report(run_report)
        run_report.photos.each do |photo|
          @printer.print(photo: photo, subject: @runner)
        end
      end
    end
    

    现在有趣的是,我们将两个对象传递给构造函数,而@runner 只传递给@printer 的#print 方法。这可能表明@runner 根本不属于这里:

    class MakePrints
      def initialize(printer)
        @printer = printer
      end
    
      def from_run_report(run_report)
        run_report.photos.each do |photo|
          @printer.print(photo)
        end
      end
    end
    

    我们已将 MakePrintsForRunner 简化为 MakePrints。这只需要在构建时使用打印机,并在方法调用时使用报告。使用哪个跑步者的复杂性现在由新的“打印机”角色负责。

    请注意,打印机是一个角色,不一定是单个类。您可以将实现换成不同的打印策略。

    现在测试应该更简单了:

    photo1 = double('photo')
    photo2 = double('photo')
    run_report = double('run report', photos: [photo1, photo2])
    printer = double('printer')
    action = MakePrints.new(printer)
    allow(printer).to receive(:print)
    
    action.from_run_report(run_report)
    
    expect(printer).to have_received(:print).with(photo1)
    expect(printer).to have_received(:print).with(photo2)
    

    这些更改可能不适合您的域。也许跑步者不应该连接到打印机上进行多次打印。在这种情况下,也许您应该采取不同的下一步。

    未来的另一个重构可能是将#from_run_report 变为#from_photos,因为该报告仅用于收集照片。在这一点上,这门课看起来有点虚弱,可能会完全消失(每个人都在看照片并调用#print 并不太有趣)。

    现在,如何测试打印机?与 ActiveRecord 集成。这是您与外界的适配器,因此应该进行集成测试。如果它真的只是创建一个记录,我可能甚至都不会费心去测试它——它只是一个 ActiveRecord 调用的包装器。

    【讨论】:

      【解决方案3】:

      类名只是常量,因此您可以使用 stub_conststub an undefined constant 并返回一个双精度值。

      因此,不要在 before(:each) 块中定义一个类,而是这样做:

      before(:each) do
        stub_const('Print', double(create: nil))
        @service.create_print_from_run_report(run_report)
      end
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-02-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-07-07
        • 2022-01-11
        相关资源
        最近更新 更多