Search code examples
ruby-on-railsrubyrspecdryrspec-rails

How to add scope-like method in a controller spec


Another variation of this question: How to test controller scopes in DRY style?

I have a lot of scopes in controller:

class SettingsController < ApplicationController
  before_filter :authenticate, :require_admin
  before_filter :navigation, only: [:show, :edit]

  def show
    flash[:notice] = I18n.t(:thank_you_for_your_subscription) if params[:p]
  end

  def edit
    render_not_found and return unless %w(username password).include?(params[:section])

    @section = params[:section]
  end
...

So, I have a lot of similar methods in controller specs (for actions). The typical controller spec is:

describe SettingsController do
  let!(:user) { Factory(:user) }

  describe "GET #show" do
    def do_action
      get :show
    end

    it_requires_sign_in
    it_requires_admin
    it_specifies_navigation_with 'settings'

    it "should set a flash message when the 'p' param is set" do
      sign_in_as user
      do_action
      flash[:notice].should_not be_nil
    end
  end

  describe "GET #edit" do
    def do_action options = {}
      get :edit, options.merge(format: 'rjs')
    end

    it_requires_sign_in
    it_requires_admin
    it_specifies_navigation_with 'settings'

    context "when singed in" do
      before { sign_in_as user }

      it "should render 404 if no section is provided" do
        do_action
        response.code.should == '404'
      end
    end
  end
end

But it_requires_sign_in method has next representation:

def it_requires_sign_in
  it "should require sign in" do
    send do_action
    response.should redirect_to(sign_in_path)
  end
end

Navigation:

def it_specifies_navigation_with(name)
  it "specifies navigation with '#{name}'" do
    controller.should receive(:current_navigation).with(name)
    sign_in_as user
    do_action
  end
end

So, it uses method from describe part. Is it possible to make some kind of scope in which i could specify methods (actions), that i want to check or not, like:

describe SettingsController do
  requires_sign_in # every action (inner describe parts)
  requires_admin
  specifies_navigation_with only: [:show, :edit]

  let!(:user) { Factory(:user) }

  describe "GET #show" do
    def do_action
      get :show
    end

    it "should set a flash message when the 'p' param is set" do
      sign_in_as user
      do_action
      flash[:notice].should_not be_nil
    end
  end
...

The main problem is to call the exact method do_action in precise scope (i mean describe action part).


Solution

  • I would not focus to dry to much. Specs are a kind of documentation. Therefore I think specs should be:

    • readable: Trying to much dry may lead to specs that are not easy to read and understand.
    • easy to change: It is likely that your need to change an action in your controller. This should be possible without changing specs for other actions.

    Therefore I think it is okay to repeat yourself in specs.

    If I had to spec that part of the controller I would start with defining a shared example:

    # shared example for authenticated admins 
    RSpec.shared_examples 'an action only for authenticated admins' do
      describe 'before_filter :authenticate' do
        context 'with an user signed in' do
          before { sign_in_as(user) }
    
          it 'does not redirect' do
            expect(response).to_not redirect_to(sign_in_path)
          end
    
        end
    
        context 'without an user' do
          it 'redirects to the sign in page' do
            expect(response).to redirect_to(sign_in_path)
          end
        end
      end
    
      describe 'before_filter :require_admin' do
        before { sign_in_as(user) }
    
        context 'when user is an admin' do
          before { allow(user).to receive(:admin?).and_return(true) }
    
          it 'does something' do
            # check something
          end
        end
    
        context 'when user is not an admin' do
          before { allow(user).to receive(:admin?).and_return(false) }
    
          it 'does something other' do
            # check something other
          end
        end
      end
    
      describe 'before_filter :navigation' do
        before do 
          allow(controller).to receive(:current_navigation).and_return_original
          allow(user).to receive(:admin?).and_return(true) 
          sign_in_as(user)
        end
    
        it 'sets the current navigation name' do
          response
          expect(controller).to have_received(:current_navigation).with(navigation_name)
        end
      end
    end
    

    See the documentation for more examples how to use shared examples.

    In the controller spec itself you will notice that I am very explicit about every single step preparing my example and the different contexts. Furthermore I prefer the new expect(...).to syntax over the old (...).should syntax.

    describe SettingsController do
      let(:user) { Factory(:user, :admin => true) }
    
      describe 'GET show' do
        subject(:response) { get :show }
    
        it_behaves_like 'an action only for authenticated admins' do
          let(:navigation_name) { 'settings' }
        end
    
        context 'with an admin signed in' do
          before { sign_in_as(user) }
    
          it 'shows a flash message' do
            response
            expect(flash[:notice]).to_not be_nil
          end
        end
      end
    
      describe 'GET edit' do
        subject(:response) { get :edit, { format: 'rjs' } }
    
        it_behaves_like 'an action only for authenticated admins' do
          let(:navigation_name) { 'settings' }
        end
    
        context 'with an admin signed in' do
          before { sign_in_as(user) }
    
          it 'shows a flash message' do
            expect(response.code).to eq('404')
          end
        end
      end
    end