jelder jelder - 4 months ago 77
Ruby Question

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.

Answer

You will find many advise telling you to use shared example 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