How to test a Controller Concern in Rails 4

前端 未结 4 1411
孤独总比滥情好
孤独总比滥情好 2021-01-29 23:18

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
    ex         


        
相关标签:
4条回答
  • 2021-01-29 23:38

    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
    
    0 讨论(0)
  • 2021-01-29 23:42

    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.

    0 讨论(0)
  • 2021-01-29 23:42

    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.

    0 讨论(0)
  • 2021-01-29 23:44

    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
    
    0 讨论(0)
提交回复
热议问题