21
votes

I was trying to test a simple index view, which has following code inside:

- if can? :destroy, MyModel
  %th Options

MyModelsController has following options (Inherited Resources + CanCan + Devise):

class MyModelsController < ApplicationController
  inherit_resources
  nested_belongs_to :mymodel
  before_filter :authenticate_user!
  load_and_authorize_resource :project
  load_and_authorize_resource :mymodel, :through => :project

When running specs, it crashes at the line - if can? :destroy, MyModel

Failure/Error: render
   ActionView::Template::Error:
      undefined method `authenticate' for nil:NilClass

There's no traceback, nothing to base on...

I thought that maybe I'm not authorized and signed when testing views, but Devise::TestHelpers should only be included in controller tests (and that's how I have it).

I was trying to override method can? in both Ability and the controller, but that gave no effect.

6

6 Answers

29
votes

This is described in the CanCan docs for controller testing, and can also be modified to apply to view specs. Here's one way to do it:

require 'spec_helper'

describe "mymodel/index.html.erb" do
  before(:each) do
    assign(:my_model,mock_model(MyModel))
    @ability = Object.new
    @ability.extend(CanCan::Ability)
    controller.stub(:current_ability) { @ability }
  end

  context "authorized user" do
    it "can see the table header" do
      @ability.can :destroy, MyModel
      render
      rendered.should have_selector('th:contains("Options")')
    end
  end

  context "unauthorized user" do
    it "cannot see the table header" do
      render
      rendered.should_not have_selector('th:contains("Options")')
    end
  end
end
6
votes

The 'before :each' code posted by zetetic doesn't work for me. My views bork on the 'can?' method because 'current_ability' in the view returns nil. I fixed it by using this 'before :each' code instead:

@ability = Ability.new(user)
assign(:current_ability, @ability)
controller.stub(:current_user, user)
view.stub(:current_user, user)

The above code simulates a login.

5
votes

In your spec_helper:

config.include Devise::TestHelpers, :type => :view

In your view spec:

controller.stub!(current_user: [some user])
view.stub!(current_user: [some user])
2
votes

For new RSpec 3.0 syntax

  before(:each) do
    assign(:my_model,mock_model(MyModel))
    @ability = Object.new.extend(CanCan::Ability)
    allow(controller).to receive(:current_ability).and_return(@ability)
  end
1
votes

The problem with the solution from the CanCan wiki is that it requires a @ability. can ... in each example, which doesn't feel very DRY.

Moreover, it doesn't actually stub out the abilities themselves, but the method that returns the controller's ability. The ability is not a stub and consequently the abilities are checked.

If you're using Rspec and want to test just the controller (and not it's abilities), here's how to stub it out:

before(:each) do
  ability = mock(:ability).as_null_object
  controller.stub(:current_ability).and_return(ability)
end

This works because as_null_object returns truthy values for all methods, so the ability checking methods pass.

0
votes

Based on John Kloian's example I defined this useful helper:

# spec/support/sign_in.rb
module ViewSpecSignInHelper
  def login_as(user)
    allow(view).to       receive(:signed_in?).and_return   true
    allow(controller).to receive(:current_user).and_return user
  end
end

RSpec.configure do |config|
  config.include ViewSpecSignInHelper, type: :view
end

My full spec/support/sign_in.rb looks like this:

module ControllerSpecSignInHelper
  def login_as(user)
    sign_in(user)
  end
end

module FeatureSpecSignInHelper
  # See https://github.com/plataformatec/devise/wiki/How-To%3a-Test-with-Capybara
  include Warden::Test::Helpers
  Warden.test_mode!

  # A login_as(user) method is provided already!
end

module ViewSpecSignInHelper
  def login_as(user)
    allow(view).to       receive(:signed_in?).and_return   true
    allow(controller).to receive(:current_user).and_return user
  end
end

RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::ControllerHelpers, type: :view

  config.include ControllerSpecSignInHelper, type: :controller
  config.include FeatureSpecSignInHelper, type: :feature
  config.include ViewSpecSignInHelper, type: :view
end

I can now login a user the same way in feature, controller, and view specs:

user = create :user # Using FactoryBot
login_as user