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.
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 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
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 an 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 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
As you can see, we define the anonymous controller with controller(ApplicationController)
. If your test concerne another class than ApplicationController
, you will need to adapt this.
Also for this to work properly you must configure the following 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
it 'includes MyControllerConcern' do
expect(SomeTargetedController.ancestors.include? MyControllerConcern).to be(true)
end
end