【问题标题】:How to test a Controller Concern in Rails 4如何在 Rails 4 中测试控制器关注点
【发布时间】:2014-02-26 23:34:12
【问题描述】:

在 Rails 4 控制器中使用时处理关注点测试的最佳方法是什么?假设我有一个琐碎的问题Citations

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.

【问题讨论】:

    标签: ruby-on-rails rspec controller activesupport-concern


    【解决方案1】:

    您会发现许多建议告诉您使用共享示例并在您包含的控制器范围内运行它们。

    我个人觉得这太过分了,我更喜欢单独执行单元测试,然后使用集成测试来确认我的控制器的行为。

    方法一:不做路由或响应测试

    创建一个假控制器并测试其方法:

    describe MyControllerConcern do
      before do
        class FakesController < ApplicationController
          include MyControllerConcern
        end
      end
    
      after do
        Object.send :remove_const, :FakesController 
      end
    
      let(:object) { FakesController.new }
    
      it 'my_method_to_test' do
        expect(object).to eq('expected result')
      end
    
    end
    

    方法二:测试响应

    当您的关注点包含路由或您需要测试响应、渲染等时...您需要使用匿名控制器运行测试。这使您可以访问所有与控制器相关的 rspec 方法和助手:

    describe MyControllerConcern, type: :controller do
      controller(ApplicationController) do
        include MyControllerConcern
    
        def fake_action; redirect_to '/an_url'; end
      end
    
      before do
        routes.draw {
          get 'fake_action' => 'anonymous#fake_action'
        }
      end
        
      describe 'my_method_to_test' do
        before do
          get :fake_action 
        end
    
        it do
          expect(response).to redirect_to('/an_url') 
        end
      end
    end
    

    如您所见,我们使用controller(ApplicationController) 定义匿名控制器。如果您的测试涉及 ApplicationController 以外的其他类,则需要调整它。

    此外,为了使其正常工作,您必须在 spec_helper.rb 文件中配置以下内容:

    config.infer_base_class_for_anonymous_controllers = true
    

    注意:继续测试您的担忧是否包括在内

    测试您的关注类是否包含在目标类中也很重要,一行就足够了:

    describe SomeTargetedController do
      it 'includes MyControllerConcern' do
        expect(SomeTargetedController.ancestors.include? MyControllerConcern).to be(true) 
      end
    end
    

    【讨论】:

    • 这个测试肯定很重要!因为它单独测试关注点,但是如果您在 3 个控制器之间共享这些并且您没有共享示例。如果有人以某种方式从您的控制器中获取包含 MyControllerConcern,则测试不会失败,并且错误将通过不明显的...所以即使您进行了隔离测试,您仍然需要共享示例来确保您的控制器执行他们应该做的事情做。在那种情况下,做这个测试是多余的,因为你应该已经有了共享的例子......
    • 这就是您使用集成的原因。这里的主题是单元测试。你是对的:对 Concern 模块的包含进行单元测试是个好主意:只测试祖先就足够了。
    • 仅供参考:Object#remove_const 是私有的,这就是为什么我们必须使用 send
    • 我可以在第一种方法的FakesController中设置params吗?
    • @mridula 你可以:object.params = { year: '2012' }
    【解决方案2】:

    从投票最多的答案中简化方法 2。

    我更喜欢 rspec 支持的 anonymous controller 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 并设置类型,以防默认情况下不会发生这种情况。

    【讨论】:

    • 这就是我在回答中“方法2”中描述的方法。
    • @BenjaminSinclaire 它有点扭曲,routes.draw 实际上不是必需的,我在尝试你的答案时发现它有点混乱,并且源链接是一个很好的补充。
    • 注意,匿名控制器默认只定义资源路由,如果你想要自定义操作,你仍然需要通过relishapp.com/rspec/rspec-rails/docs/controller-specs/…调用routes.draw
    • 我已经搜索了一段时间来测试由关注点添加的路线。据你所知,这是可行的吗?如果没有,我不会费心问问题
    • 应该是,我不明白为什么,请提出问题,并会尽力为您提供答案。
    【解决方案3】:

    我的回答可能看起来比@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 时尤其重要。在这种情况下,在创建新版本时复制大量控制器是很常见的,然后很容易犯这种微妙的复制粘贴错误。

    【讨论】:

      【解决方案4】:

      我正在使用一种更简单的方法来测试我的控制器问题,不确定这是否是正确的方法,但似乎比上述方法简单得多,而且对我来说很有意义,它使用了包含的控制器的范围。如果此方法有任何问题,请告诉我。 示例控制器:

      class MyController < 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)    { MyController.new }
        context '#column_type' do
          it 'should return :phone for phone type column' do
            expect(ac.column_type('phone')).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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-10
        • 2014-08-15
        • 1970-01-01
        相关资源
        最近更新 更多