2014-02-11 60 views
4

我遵循TDD方法來構建我們的應用程序,並創建一大堆服務對象,嚴格保持模型的數據管理。在服務測試中剔除ActiveRecord模型

我建立的許多服務都與模型接口。舉個例子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 

我很欣賞的創建方法也可以說是被抽象成打印模式,但讓我們保持它作爲是現在。

現在,在MakePrintsForRunner的規範中,我非常希望避免包含spec_helper,因爲我希望我的服務規範超快。

相反,我踩滅打印類是這樣的:

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

您不必創建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 
+0

打印類沒有加載,雖然。我明確地試圖避免包含print.rb,因爲它依賴於ActiveRecord,因此會使spec測試變得緩慢。 運行您的代碼將導致: 'NameError: 未初始化的常量Print' – idrysdale

+0

是從您的代碼中調用的'reflect_on_association'方法嗎? –

+0

不,它會在整個測試套件的其他地方打印,因爲它只是一個「ActiveRecord :: Reflection」方法 – idrysdale

0

I appreciate the create method could arguably be abstracted into the Print model, but let's keep it as is for now.

讓我們看看如果我們忽略這條線會發生什麼。

你在困難的課堂上遇到的困難表明設計不靈活。考慮將已經實例化的對象傳遞給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,因爲報告不會用於​​收集照片。在這一點上,這個班看起來有點貧血,可能會完全消失(分享照片和打電話#打印不是太有趣)。

現在,如何測試打印機?與ActiveRecord集成。這是您對外部世界的適配器,因此應該進行集成測試。如果它真的只是創建一個記錄,我可能甚至不會去測試它 - 它只是一個ActiveRecord調用的包裝。

0

類名只是常數,所以你可以使用stub_conststub an undefined constant並返回一個double。

因此,而不是在你的before(:each)塊定義一個類的做到這一點:

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