2014-02-26 82 views
32

在Rails 4控制器中使用時處理關注點測試的最佳方式是什麼?說我有一個微不足道的關注Citations如何在Rails 4中測試Controller關注點

module Citations 
    extend ActiveSupport::Concern 
    def citations ; end 
end 

被測預期的行爲是任何控制器,其中包括這個問題會得到這個citations端點。

class ConversationController < ActionController::Base 
    include Citations 
end 

簡單。

ConversationController.new.respond_to? :yelling #=> true 

但是,單獨測試這種擔憂的正確方法是什麼?

class CitationConcernController < ActionController::Base 
    include Citations 
end 

describe CitationConcernController, type: :controller do 
    it 'should add the citations endpoint' do 
     get :citations 
     expect(response).to be_successful 
    end 
end 

不幸的是,這將失敗。

CitationConcernController 
    should add the citations endpoint (FAILED - 1) 

Failures: 

    1) CitationConcernController should add the citations endpoint 
    Failure/Error: get :citations 
    ActionController::UrlGenerationError: 
     No route matches {:controller=>"citation_concern", :action=>"citations"} 
    # ./controller_concern_spec.rb:14:in `block (2 levels) in <top (required)>' 

這是一個人爲的例子。在我的應用程序中,我收到了另一個錯誤。

RuntimeError: 
    @routes is nil: make sure you set it in your test's setup method. 

回答

70

你會發現許多建議,告訴你使用共享的例子,並在你包含的控制器範圍內運行它們。

我個人發現它過度查殺,更喜歡單獨進行單元測試,然後使用集成測試來確認我的控制器的行爲。

方法1:沒有路由或響應測試

創建一個假的控制器,並測試其方法:

describe MyControllerConcern do 

    before do 
    class FakesController < ApplicationController 
     include MyControllerConcern 
    end 
    end 
    after { Object.send :remove_const, :FakesController } 
    let(:object) { FakesController.new } 

    describe 'my_method_to_test' do 
    it { expect(object).to eq('expected result') } 
    end 

end 

方法2:測試響應

當你的關心包含路由或者您需要測試響應,渲染等......您需要使用匿名控制器運行測試。這讓你獲得所有控制器相關rspec的方法和助手訪問:

describe MyControllerConcern, type: :controller do 

    controller(ApplicationController) do 
    include MyControllerConcern 

    def fake_action; redirect_to '/an_url'; end 
    end 
    before { routes.draw { 
    get 'fake_action' => 'anonymous#fake_action' 
    } } 


    describe 'my_method_to_test' do 
    before { get :fake_action } 
    it { expect(response).to redirect_to('/an_url') } 
    end 
end 

你可以看到,我們有一個controller(ApplicationController)來包裝匿名控制器。如果你的課程是從ApplicationController以外的課程中繼承的,那麼你需要適應這一點。

而且使其正常工作,你必須在你的spec_helper.rb聲明文件:

config.infer_base_class_for_anonymous_controllers = true 

注:保持測試,你關注的是包括

同樣重要的是,以測試您的關注級別包含在您的目標級別中,一條線就足夠了:

describe SomeTargetedController do 
    describe 'includes MyControllerConcern' do 
    it { expect(SomeTargetedController.ancestors.include? MyControllerConcern).to eq(true) } 
    end 
end 
+1

這個測試可以絕對重要!因爲它會孤立地測試問題,但是如果您在3個控制器之間共享這些問題並且您沒有共享示例。如果有人從某個控制器中接受了包含MyControllerConcern的測試,測試不會失敗,並且錯誤會通過不明顯的...所以即使您進行了隔離測試,您仍然需要共享示例來確保您的控制器執行他們應該做的做。在這種情況下,進行此測試是一種矯枉過正的行爲,因爲您應該已經有了共享示例...... –

+2

這就是爲什麼您要使用集成。這裏的主題是單元測試。你是對的:單元測試包括關注模塊是一個好主意:只需對祖先進行測試就足夠了。 – Benj

+1

僅供參考:'Object#remove_const'是私有的,這就是爲什麼我們必須使用'send' – Dorian

21

從投票數最多的答案中簡化方法2。

我寧願支持anonymous controller在RSpec的http://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/anonymous-controller

你會做:

describe ApplicationController, type: :controller do 
    controller do 
    include MyControllerConcern 

    def index; end 
    end 

    describe 'GET index' do 
    it 'will work' do 
     get :index 
    end 
    end 
end 

請注意,你需要描述ApplicationController和設置的情況下,這不會默認執行的類型。

+0

這是我在答案中的「方法2」中描述的方法。 – Benj

+0

@BenjaminSinclaire它有一點點扭曲它,routes.draw實際上並不是必需的,我發現它在嘗試你的答案時有點困惑,並且鏈接到源代碼是一個很好的補充。 – Calin

+3

注意,匿名控制器默認定義足智多謀的路由,如果你想定製動作,你仍然需要通過http://www.relishapp調用'routes.draw'。COM/RSpec的/ RSpec的護欄/文檔/控制器規格/匿名控制器#匿名控制器只創建資源的路由 – Epigene

1

我的答案可能比@Benj和@Calin看起來更復雜一點,但它有其優點。所有的

describe Concerns::MyConcern, type: :controller do 

    described_class.tap do |mod| 
    controller(ActionController::Base) { include mod } 
    end 

    # your tests go here 
end 

首先,我建議使用匿名控制器這是ActionController::Base一個子類,不ApplicationController無論是在應用程序中定義的任何其它基本控制器。通過這種方式,您可以單獨測試您的任何控制器的問題。如果您希望在基本控制器中定義一些方法,只需對它們進行存根。

此外,避免重新輸入關注模塊名稱是個好主意,因爲它有助於避免複製粘貼錯誤。遺憾的是,described_class在傳遞給controller(ActionController::Base)的塊中無法訪問,因此我使用#tap方法創建另一個將described_class存儲在局部變量中的綁定。使用版本化的API時,這一點尤爲重要。在這種情況下,在創建新版本時複製大量控制器是非常常見的,然後很容易做出如此微妙的複製粘貼錯誤。

0

我正在使用一種更簡單的方式來測試我的控制器問題,不確定這是否是正確的方法,但似乎更簡單,以上和我的意義,它使用您的包含的控制器的範圍。請讓我知道這個方法是否有問題。 樣品控制器:

class MyController < V1::BaseController 
    include MyConcern 

    def index 
    ... 

    type = column_type(column_name) 
    ... 
    end 

我的控制器的擔憂:

module MyConcern 
    ... 
    def column_type(name) 
    return :phone if (column =~ /phone/).present? 
    return :id if column == 'id' || (column =~ /_id/).present? 
    :default 
    end 
    ... 

end 

規範測試關注:

require 'spec_helper' 

describe SearchFilter do 
    let(:ac) { V1::AppointmentsController.new } 
    context '#column_type' do 
    it 'should return :phone for phone type column' do 
     expect(ac.column_type('phone_daytime')).to eq(:phone) 
    end 

    it 'should return :id for id column' do 
     expect(ac.column_type('company_id')).to eq(:id) 
    end 

    it 'should return :id for id column' do 
     expect(ac.column_type('id')).to eq(:id) 
    end 

    it 'should return :default for other types of columns' do 
     expect(ac.column_type('company_name')).to eq(:default) 
    end 
    end 
end 
相關問題