47

在 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.
4

4 回答 4

94

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

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

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

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

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

方法2:测试响应

当您关心的问题包含路由或您需要测试响应、渲染等时......您需要使用匿名控制器运行测试。这允许您访问所有与控制器相关的 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
于 2014-02-27T03:49:53.233 回答
29

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

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

于 2015-06-05T13:59:02.977 回答
5

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

于 2016-07-29T21:44:29.847 回答
-1

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

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
于 2017-04-26T20:40:31.910 回答