问题
What is the best way to handle testing of concerns when used in Rails 4 controllers? Say I have a trivial concern Citations
.
module Citations
extend ActiveSupport::Concern
def citations ; end
end
The expected behavior under test is that any controller which includes this concern would get this citations
endpoint.
class ConversationController < ActionController::Base
include Citations
end
Simple.
ConversationController.new.respond_to? :yelling #=> true
But what is the right way to test this concern in isolation?
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
Unfortunately, this fails.
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)>'
This is a contrived example. In my app, I get a different error.
RuntimeError:
@routes is nil: make sure you set it in your test's setup method.
回答1:
You will find many advice telling you to use shared examples and run them in the scope of your included controllers.
I personally find it over-killing and prefer to perform unit testing in isolation then use integration testing to confirm the behavior of my controllers.
Method 1: without routing or response testing
Create a fake controller and test its methods:
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
Method 2: testing response
When your concern contains routing or you need to test for response, rendering etc... you need to run your test with anonymous controller. This allow you to gain access to all controller-related rspec methods and helpers:
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
You can see that we have to wrap the anonymous controller in a controller(ApplicationController)
. If your classes are inherited from another class than ApplicationController
, you will need to adapt this.
Also for this to work properly you must declare in your spec_helper.rb file:
config.infer_base_class_for_anonymous_controllers = true
Note: keep testing that your concern is included
It is also important to test that your concern class is included in your target classes, one line suffice:
describe SomeTargetedController do
describe 'includes MyControllerConcern' do
it { expect(SomeTargetedController.ancestors.include? MyControllerConcern).to eq(true) }
end
end
回答2:
Simplifying on method 2 from the most voted answer.
I prefer the anonymous controller
supported in rspec http://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/anonymous-controller
You will do:
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
Note that you need to describe the ApplicationController
and set the type in case this does not happen by default.
回答3:
My answer may look bit more complicated than these by @Benj and @Calin, but it has its advantages.
describe Concerns::MyConcern, type: :controller do
described_class.tap do |mod|
controller(ActionController::Base) { include mod }
end
# your tests go here
end
First of all, I recommend the use of anonymous controller which is a subclass of ActionController::Base
, not ApplicationController
neither any other base controller defined in your application. This way you're able to test the concern in isolation from any of your controllers. If you expect some methods to be defined in a base controller, just stub them.
Furthermore, it is a good idea to avoid re-typing concern module name as it helps to avoid copy-paste errors. Unfortunately, described_class
is not accessible in a block passed to controller(ActionController::Base)
, so I use #tap
method to create another binding which stores described_class
in a local variable. This is especially important when working with versioned APIs. In such case it is quite common to copy large volume of controllers when creating a new version, and it's terribly easy to make such a subtle copy-paste mistake then.
回答4:
I am using a simpler way to test my controller concerns, not sure if this is the correct way but seemed much simpler that the above and makes sense to me, its kind of using the scope of your included controllers. Please let me know if there are any issues with this method. sample controller:
class MyController < BaseController
include MyConcern
def index
...
type = column_type(column_name)
...
end
end
my controller concern:
module MyConcern
...
def column_type(name)
return :phone if (column =~ /phone/).present?
return :id if column == 'id' || (column =~ /_id/).present?
:default
end
...
end
spec test for concern:
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
来源:https://stackoverflow.com/questions/22055889/how-to-test-a-controller-concern-in-rails-4