Even a seemingly simple index action feels incredibly complicated to test in isolation.
I find myself having to mock out several of my User and Tenant methods just to get through the before_filters. Then I need to mock out Kaminari and Tenant#users for the action.
This feels excessive for testing a controller action with no control flow.
TDD principle would say that an excessive need for mocking is a sign of poor design, but then I'm not sure how I would extract this functionality into a domain object.
Is this sort of painful mocking standard for testing Rails controllers? Is there better way to do this that I'm simply not aware of?
For instance, perhaps skipping before_filters would make this less painful, but as they are consequential private methods, I feel that skipping them is missing the point.
class UsersController < AdminController
before_filter :check_auth
before_filter :check_admin
around_filter :set_tenant_time_zone, if: current_tenant
def index
Kaminari.paginate(current_tenant.users).page(params[:page])
end
private
def current_user
# gets user from session
end
def current_tenant
current_user.tenant if current_user
end
def set_tenant_time_zone
Time.use_zone(current_tenant.time_zone, &block)
end
def check_auth
redirect_to login_url unless AuthChecker.new(current_user, request.remote_ip).has_access?
end
def check_admin
redirect_to root_url unless current_user.is_admin?
end
end
You have to do all those mocks/stubs if you want to run those before_filters but I think, that, for those cases, is better to use some spec helper method to create a logged in user so, on your spec, you only need to call that method on a "before(:each)" block of your controller where you want a user.
In spec_helper.rb:
def current_user(stubs = {})
unless #current_user
u = FactoryGirl.build(:user, stubs)
u.save(:validate => false)
#current_user = u
end
#current_user
end
def current_user_session(stubs = {}, user_stubs = {})
#current_session ||= mock_model("Session", {:record => nil, :user => current_user(user_stubs)}.merge(stubs))
end
def login(session_stubs = {}, user_stubs = {})
UserSession.stub(:find).and_return(current_user_session(session_stubs, user_stubs))
controller.stub(:current_user => #current_user)
end
so, on the controller specs that require a logged in user with some special stub I can do
describe 'GET index' do
before(:each) do
login #this does all you need to pass the filters
end
it 'does something' do
current_user.stub(:some_method)
get :index
expect(response).to something
end
end
that way the test only has stubs, instances and expectations for the actual code of the action and not the filters
Related
I'm writing RSpec tests for a Rails 4.2 application which uses Pundit for authorization.
I'd like to test whether authorization is enforced in all actions of all controllers, to avoid unintentionally providing public access to sensitive data in case a developer forgets to call policy_scope (on #index actions) and authorize (on all other actions).
One possible solution is to mock these methods in all controller unit tests. Something like expect(controller).to receive(:authorize).and_return(true) and expect(controller).to receive(:policy_scope).and_call_original. However, that would lead to a lot of code repetition. This line could be placed within a custom matcher or a helper method in spec/support but calling it in every spec of every controller also seems repetitive. Any ideas on how to achieve this in a DRY way?
In case you are wondering, Pundit's policy classes are tested separately, as shown in this post.
Pundit already provides a mechanism to guarantee a developer can't forget to authorize during the execution of a controller action:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
end
This instructs Pundit to raise if the auth wasn't performed. As long as all your controllers are tested, this will cause the spec to fail.
https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
I feel like you could use something like this up in spec_helper. Note that I'm assuming a naming convention where you have the word "index" in the index level answers, so that your spec might look like this:
describe MyNewFeaturesController, :type => :controller do
describe "index" do
# all of the index tests under here have policy_scope applied
end
# and these other tests have authorize applied
describe 'show' do
end
describe 'destroy' do
end
end
and here is the overall configuration:
RSpec.configure do |config|
config.before(:each, :type => :controller) do |spec|
# if the spec description has "index" in the name, then use policy-level authorization
if spec.metadata[:full_description] =~ /\bindex\b/
expect(controller).to receive(:policy_scope).and_call_original
else
expect(controller).to receive(:authorize).and_call_original
end
end
end
Here is an example using shared_examples, the before :suite hook, and metaprogramming that might get at what you need.
RSpec.configure do |config|
config.before(:suite, :type => :controller) do |spec|
it_should_behave_like("authorized_controller")
end
end
and over in spec_helper
shared_examples_for "authorized_controller" do
# expects controller to define index_params, create_params, etc
describe "uses pundit" do
HTTP_VERB = {
:create => :post, :update=>:put, :destroy=>:delete
}
%i{ new create show edit index update destroy}.each do |action|
if controller.responds_to action
it "for #{action}" do
expect(controller).to receive(:policy_scope) if :action == :index
expect(controller).to receive(:authorize) unless :action == :index
send (HTTP_VERB[action]||:get), action
end
end
end
end
end
I'm posting the code for my latest attempt.
Please note that:
You should probably not use this code as it feels overly complex and hacky.
It does not work if authorize or policy_scope is called after an exception happens. Exceptions will occur if a tested action calls Active Record methods such as find, update and destroy without providing them valid parameters. The following code creates fake parameters with empty values. An empty ID is invalid and will result in a ActiveRecord::RecordNotFound exception. Will update the code once I find a solution for this.
spec/controllers/all_controllers_spec.rb
# Test all descendants of this base controller controller
BASE_CONTROLLER = ApplicationController
# To exclude specific actions:
# "TasksController" => [:create, :new, :index]
# "API::V1::PostsController" => [:index]
#
# To exclude entire controllers:
# "TasksController" => nil
# "API::V1::PostsController" => nil
EXCLUDED = {
'TasksController' => nil
}
def expected_auth_method(action)
action == 'index' ? :policy_scope : :authorize
end
def create_fake_params(route)
# Params with non-nil values are required to "No route matches..." error
route.parts.map { |param| [param, ''] }.to_h
end
def extract_action(route)
route.defaults[:action]
end
def extract_http_method(route)
route.constraints[:request_method].to_s.delete("^A-Z")
end
def skip_controller?(controller)
EXCLUDED.key?(controller.name) && EXCLUDED[controller.name].nil?
end
def skip_action?(controller, action)
EXCLUDED.key?(controller.name) &&
EXCLUDED[controller.name].include?(action.to_sym)
end
def testable_controllers
Rails.application.eager_load!
BASE_CONTROLLER.descendants.reject {|controller| skip_controller?(controller)}
end
def testable_routes(controller)
Rails.application.routes.set.select do |route|
route.defaults[:controller] == controller.controller_path &&
!skip_action?(controller, extract_action(route))
end
end
# Do NOT name the loop variable "controller" or it will override the
# "controller" object available within RSpec controller specs.
testable_controllers.each do |tested_controller|
RSpec.describe tested_controller, :focus, type: :controller do
# login_user is implemented in spec/support/controller_macros.rb
login_user
testable_routes(tested_controller).each do |route|
action = extract_action(route)
http_method = extract_http_method(route)
describe "#{http_method} ##{action}" do
it 'enforces authorization' do
expect(controller).to receive(expected_auth_method(action)).and_return(true)
begin
process(action, http_method, create_fake_params(route))
rescue ActiveRecord::RecordNotFound
end
end
end
end
end
end
My AdminController looks like:
class AdminController < ApplicationController
before_action :check_admin
private
def check_admin
redirect_to 'home/error' unless current_user.admin?
end
end
In my rspec test, how can I test this if there are no route or views?
require 'rails_helper'
RSpec.describe AdminController, type: :controller do
context "with no render_views" do
it "redirects for non-admin users" do
#???expect do
end
end
end
I am assuming that you are using a before_action in your AdminController, even though this controller does not have any actions, so that any controllers that inherit from it will automatically by "admin only".
If so, there are two ways to approach testing this.
1) Don't write a test for check_admin.
Instead, write tests for any controller actions that you define later! For example, if you have the following controller in your application tomorrow:
UsersController < AdminController
def index
#users = User.all
end
end
then you can write the following specs for that controller.
describe UsersController
it 'redirects for non-admins' do
# insert the test you feel like writing here!
end
it 'renders the right template for admin users' do
magical_login_method
get :index
expect(response).to render_template(:index)
end
end
and so on!
2) Call the private method directly
This approach makes me feel a bit icky. Although this defeats the philosophy of public vs private methods, you can call a private method in ruby by using the .send method.
describe AdminController
it 'redirects for non-admins' do
# make an instance of your controller
controller = AdminController.new
# expect the controller to call `redirect_to`
expect(controller).to receive(:redirect_to).with('home/error')
# call the private `check_admin` method
controller.send(:check_admin)
end
end
Some, perhaps many, would argue that this sort of testing is highly intrusive, and may even limit the flexibility of your codebase in the future. I'd recommend approach 1, not because it's lazy, but because it tests things once there's something to test!
Ok, so my main issue is I have implemented Mailboxer into our project to handle messaging and I am trying to write tests for it. However, I keep stumbling over and over again. I have attempted several different stub/mocks but have made no progress.
We have a conversations_controller.rb that relies on before_filters for setting all the instance variables necessary for doing each action. Then in the controller actions, the instance variables are referenced directly to do any sort of action or to return specific data.
Here is an example of our index action which returns all conversations in the "box" that is specified in the before_filter, of the mailbox also specified in another before_filter:
class ConversationsController < ::ApplicationController
before_filter :get_user_mailbox, only: [:index, :new_message, :show_message, :mark_as_read, :mark_as_unread, :create_message, :reply_message, :update, :destroy_message, :untrash]
before_filter :get_box
def index
if #box.eql? "inbox"
#conversations = #mailbox.inbox
elsif #box.eql? "sentbox"
#conversations = #mailbox.sentbox
else
#conversations = #mailbox.trash
end
end
And before filters:
private
def get_user_mailbox
#user = User.where(:user_name => user.user_name.downcase).where(:email => user.email.downcase).first_or_create
#mailbox = #user.mailbox if #user
end
def get_box
if params[:box].blank? or !["inbox","sentbox","trash"].include?params[:box]
params[:box] = 'inbox'
end
#box = params[:box]
end
So I guess I have 2 questions in one. First, how to I get my tests to generate the correct data #mailbox, #user, and #box that is needed for the index action. Next, how do I pass the fake parameter to set #box to different "inbox/sentbox/trash". I have tried controller.index({box: "inbox"}) but always get "wrong arguments 1 for 0" messages.
I have tried the following in various different ways, but always get nil:class errors which means that my instance variables are definitely not being set properly.
describe "GET 'index' returns correct mailbox box" do
before :each do
#user = User.where(:user_name => 'test').where(:email => 'test#test.com').first_or_create
#mailbox = #user.mailbox
end
it "#index returns inbox when box = 'inbox'" do
mock_model User
User.stub_chain(:where, :where).and_return(#user)
controller.index.should == #mailbox.inbox
end
end
Filters and callbacks are hard to test and debug. Avoid them when possible.
In this case I don't think your before_filter is necessary, thus no need to test it. The better home for the methods is model.
Check my refacoring:
class User < ActiveRecord::Base
delegate :inbox, :sentbox, :trash, to: :mailbox
end
class ConversationsController < ::ApplicationController
def index
#conversations = current_user.send get_box
end
private
def get_box
# your code
end
end
That's all. Should be enough.
You can then test regularly.
First of all, read the oficial documentation for rails testing: using data for testing and passing parameters to controlers is explained there.
To generate data for your tests you can:
fill your test database with some mailbox and users using rails fixtures or use something like factory girl
use mock objects to fake data. I personally use the mocha gem but there are others
I tend to use a combination of both, prefering mock objects when possible and falling back to factory girl when mocking needs too much code.
I have a standard current_user in application_controller like this:
describe ApplicationController < ActionController::Base
helper_method :current_user
def current_user
#current_user ||= User.find_by_auth_token(session[:auth_token]) if session[:auth_token]
end
I'm curious as to how or whether if this should be tested. I was thinking like (this works but I'm not sure if there is a better way or whether even necessary):
describe 'current_user' do
it 'should return a valid_current' do
user=FactoryGirl.create(:user)
user.auth_token='abc123'
user.save
request.session['auth_token']='abc123'
returned_user=controller.send(:current_user) # not sure about this
returned_user.id.should eq(user.id)
end
end
but not really sure? I'm testing our auth in other places for our api but curious (1) how or (2) whether this should be tested (even for completeness)?
thx
In a few of my controllers I have a before_filter that checks if a user is logged in? for CRUD actions.
application.rb
def logged_in?
unless current_user
redirect_to root_path
end
end
private
def current_user_session
return #current_user_session if defined?(#current_user_session)
#current_user_session = UserSession.find
end
def current_user
return #current_user if defined?(#current_user)
#current_user = current_user_session && current_user_session.record
end
But now my functional tests fail because its redirecting to root. So I need a way to simulate that a session has been created but nothing I've tried has worked. Heres what I have right now and the tests pretty much ignore it:
test_helper.rb
class ActionController::TestCase
setup :activate_authlogic
end
posts_controller_test.rb
class PostsControllerTest < ActionController::TestCase
setup do
UserSession.create(:username => "dmix", :password => "12345")
end
test "should get new" do
get :new
assert_response :success
end
Am I missing something?
You should pass ActiveRecord object in UserSession.create
Something like:
u = users(:dmix)
UserSession.create(u)
http://rdoc.info/github/binarylogic/authlogic/master/Authlogic/TestCase
First you need to activate AuthLogic so that you can use it in your tests.
setup :activate_authlogic
Then you need a valid user record as Anton Mironov pointed out.
All I do in my rspec tests for my controller is create a User with Machinist and then assign that user to be the current_user.
def login_user(options = {})
user = User.make(options)
#controller.stub!(:current_user).and_return(user)
end
and this attaches the current_user to the controller, which would mean that your logged_in? method would work in your tests.
You obviously would probably need to adapt this to work in Test::Unit, and without Machinist if you don't use it, as I use rspec, but I'm sure the principle is the same.
Put this in test_helper.rb if you want all your tests to setup Authlogic:
class ActionController::TestCase
def self.inherited(subclass)
subclass.instance_eval do
setup :activate_authlogic
end
end
end
Here is a link to the AuthLogic test documentation. It's an important one but is a bit buried (the same link Simone posted, however his didn't work anymore).
That page has all the information you need to get going testing you application using AuthLogic for authentication.
Additionally, as railsninja suggested, use factories not fixtures. Take a look at factory_girl and machinist; pick your poison, they are both good.