我正在遵循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
这会让你感到不快乐。
关于如何解决这个问题的任何建议。虽然这是一个例子,但有很多时候服务直接引用模型的方法,我采用上述方法将它们删除。还有更好的方法吗?
答案 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_const
到stub an undefined constant并返回一个双。
因此,不要在before(:each)
块中定义类,而是执行此操作:
before(:each) do
stub_const('Print', double(create: nil))
@service.create_print_from_run_report(run_report)
end