在服务测试中删除ActiveRecord模型

时间:2014-02-11 12:48:25

标签: ruby-on-rails ruby tdd

我正在遵循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

这会让你感到不快乐。

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

3 个答案:

答案 0 :(得分:0)

您不必创建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

答案 1 :(得分:0)

  

我很欣赏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调用的包装。

答案 2 :(得分:0)

类名只是常量,所以你可以使用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