25

我曾在一個模型的方法:如何獨立測試模型的回調方法?

class Article < ActiveRecord::Base 
    def do_something 
    end 
end 

我也有過這種方法的單元測試:

# spec/models/article_spec.rb 
describe "#do_something" do 
    @article = FactoryGirl.create(:article) 
    it "should work as expected" do 
    @article.do_something 
    expect(@article).to have_something 
    end 
    # ...several other examples for different cases 
end 

一切都很好,直到我發現它是把這種方法進入一個after_save回調:

class Article < ActiveRecord::Base 
    after_save :do_something 

    def do_something 
    end 
end 

現在我所有關於此方法的測試都被破壞了。我必須解決它:

  • 沒有更具體的調用do_something因爲createsave將觸發此方法爲好,不然我會遇到重複的數據庫操作。
  • 變化createbuild
  • 測試的respond_to
  • 使用的一般model.save而不是單獨的方法調用model.do_something

    describe "#do_something" do 
        @article = FactoryGirl.build(:article) 
        it "should work as expected" do 
        expect{@article.save}.not_to raise_error 
        expect(@article).to have_something 
        expect(@article).to respond_to(:do_something) 
        end 
    end 
    

測試通過,但我關心的是它不再是具體的方法。如果更多地添加,效果將是與其他回調混合。

我的問題是,是否有任何美麗的方式來測試模型的實例方法獨立成爲一個回調?

+0

爲什麼你原來的做法不仍然可以用於測試目前尚不清楚。單元直接測試該方法,只是測試它被獨立地稱爲回調。我錯過了什麼,或者有什麼你不喜歡的方法嗎? –

+0

@AndrewHubbs,謝謝你的提問。原因是這種方法改變了分貝。例如,它會將這篇文章分配給「Rails」類別。在refacoring回調後,當我調用FactoryGirl.create時,此回調將生效並將文章分配給類別「Rails」。當我在測試中再次調用此方法時,將會有一個錯誤,因爲它已被分配。 –

回答

54

回調和回調行爲是獨立測試。如果你想檢查after_save回調,你需要把它看成兩件事情:

  1. 是否爲正確的事件觸發回調?
  2. 被調用函數是否正確?

假設你有Article類有許多回調,這是你將如何測試:

class Article < ActiveRecord::Base 
    after_save :do_something 
    after_destroy :do_something_else 
    ... 
end 

it "triggers do_something on save" do 
    expect(@article).to receive(:do_something) 
    @article.save 
end 

it "triggers do_something_else on destroy" do 
    expect(@article).to receive(:do_something_else) 
    @article.destroy 
end 

it "#do_something should work as expected" do 
    # Actual tests for do_something method 
end 

這從行爲解耦您的回調。例如,當更新其他相關對象時,您可以觸發相同的回調方法article.do_something,如user.before_save { user.article.do_something }。這將適應所有這些。

因此,像往常一樣繼續測試你的方法。分別擔心回調。

編輯:錯別字和潛在的誤解 編輯:改變「做什麼」到「觸發一些」

+0

+1「用於解除您的回調行爲。」 RDX,你對「#實際測試do_something方法」的建議是什麼?稍後使用我的方法(通過「保存」對象來測試整體行爲)? –

+0

只是'do_something'方法的實際測試,有點像您的文章中的第一個rspec:'@ article.do_something;期望(@article).to have_something'。基本上'save'是一個更大的方法,它可能會觸發很多before_saves,after_saves等等。但是如果你已經測試了所有的單個方法,並且你知道每個方法都正確地完成了它的工作,那麼你只需要寫save/before_save/after_save/...方法的測試非常少且短。從經過良好測試的小函數中構建更大的函數:) – Subhas

+0

偶然,你對http://stackoverflow.com/questions/35950470/rails-factorygirl-trait-association-with-model-after-create-callback有任何想法-not-setting –

0

這比回答的評論,但我把它放在這裏的語法高亮...

我就想辦法跳過我的測試回調,這是我做的。 (這可能有助於破解測試。)

class Article < ActiveRecord::Base 
    attr_accessor :save_without_callbacks 
    after_save :do_something 

    def do_something_in_db 
    unless self.save_without_callbacks 
     # do something here 
    end 
    end 
end 

# spec/models/article_spec.rb 
describe Article do 
    context "after_save callback" do 
    [true,false].each do |save_without_callbacks| 
     context "with#{save_without_callbacks ? 'out' : nil} callbacks" do 
     let(:article) do 
      a = FactoryGirl.build(:article) 
      a.save_without_callbacks = save_without_callbacks 
     end 
     it do 
      if save_without_callbacks 
      # do something in db 
      else 
      # don't do something in db 
      end 
     end 
     end 
    end 
    end 
end 
+0

泰迪,感謝您的回答,或評論? :)恐怕這種方式的成本有點高,在測試中增加了一些額外的代碼,並改變了模型以適應故意的測試。 –

+0

是的,我也必須種子數據庫,跳過回調是一個要求,所以我在我的情況下獲得更多的好處。 – Teddy

+0

對此有一個skip_callback方法:https://github.com/rails/rails/blob/b894b7b90a6aced0e78ab84a45bf1c75c871bb2d/activesupport/lib/active_support/callbacks.rb#L620 –

13

您可以使用shoulda-callback-matchers來測試你的回調存在不調用它們。

describe Article do 
    it { should callback(:do_something).after(:save) } 
end 

如果你也想測試回調的行爲:

describe Article do 
    ... 

    describe "#do_something" do 
    it "gives the article something" do 
     @article.save 
     expect(@article).to have_something 
    end 
    end 
end 
+0

很棒的編輯@dennis!謝謝 :) –

-1
describe "#do_something" do 

it "gives the article something" do 

    @article = FactoryGirl.build(:article) 

    expect(@article).to have_something 

@article.save 
end 

end