Testing views that use CanCan and Devise with RSpec
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.
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
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.
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])
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
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.
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
精彩评论