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).
I would not focus to dry to much. Specs are a kind of documentation. Therefore I think specs should be:
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