ruby-on-railsrspeccontrolleractivesupport-concern

How to test a Controller Concern in Rails 4


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.

Solution

  • 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