Best approach to test scope chains in Rails - ruby-on-rails

In all my ruby on rails app, I try to not use the database in controllers, since they should be independent from persistence classes. I used mocking instead.
Here is the example for rspec and rspec-mock:
class CouponsController < ApplicationController
def index
#coupons = Coupon.all
end
end
require 'spec_helper'
describe CouponsController do
let(:all_coupons) { mock }
it 'should return all coupons' do
Coupon.should_receive(:all).and_return(all_coupons)
get :index
assigns(:coupons).should == all_coupons
response.should be_success
end
end
But what if controller contains more complex scopes, like:
class CouponsController < ApplicationController
def index
#coupons = Coupon.unredeemed.by_shop(shop).by_country(country)
end
end
Do you know any good approach for testing simillar scopes chains?
I think that the following test does not look really good:
require 'spec_helper'
describe CouponsController do
it 'should return all coupons' do
Coupon.should_receive(:unredeemed).and_return(result = mock)
result.should_receive(:by_shop).with(shop).and_return(result)
result.should_receive(:by_country).with(country).and_return(result)
get :index
assigns(:coupons).should == result
response.should be_success
end
end

You could use stub_chain method for that.
Something like:
Coupon.stub_chain(:unredeemed, :by_shop, :by_country).and_return(result)
Just an example.

With rspec > 3 use this syntax:
expect(Converter).to receive_message_chain("new.update_value").with('test').with(no_args)
instead of stub_chain.
Read more about message chains in the documenation.

Related

Rails / Rspec: Having an anonymous controller be of a certain class

My controllers inherit actions from ApplicationController. My goal is to test the behaviour of any controller that inherits from ApplicationController. I created RandomController in my specs in order to achieve that goal.
Here is my spec so far
require 'rails_helper'
RSpec.configure do |c|
c.infer_base_class_for_anonymous_controllers = false
end
class RandomController < ApplicationController; end
class Random < ApplicationRecord; end
RSpec.describe RandomController, type: :controller do
controller {}
describe '.index' do
context 'when no record exists' do
before { get :index }
specify { should respond_with(200) }
end
end
end
Here is application_controller
class ApplicationController
def index
binding.pry
end
end
The issue is that when the index method runs, self.class returns #<Class:0x00007f8c33b56fc8> instead of RandomController. Is it possible to have my anonymous controller be an instance of a given controller (declared within the specs) ?
According to the docs you can specify the base class for the anonymous controller:
To specify a different base class you can pass the class explicitly to the
controller method:
controller(BaseController)
https://relishapp.com/rspec/rspec-rails/docs/controller-specs/anonymous-controller
Thus you can probably call:
controller(RandomController)
in your specs
Consider using shared_context instead of creating a RandomController to test shared code:
shared_context 'an application controller' do
describe '#index' do
context 'when no record exists' do
before { get :index }
expect(response).to have_http_status(:ok)
end
end
end
You would typically put this file under /spec/support. Example:
/spec/support/shared_contexts_for_application_controllers.rb
Then, in each controller that inherits from ApplicationController:
describe RandomController do
include_context 'an application controller'
end

Rails testing: ensure authorization (Pundit) is enforced in all controllers and actions

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

rspec - Newly instantiated record is nil according to assigns

In my controller:
class CategoriesController < ApplicationController
before_filter :authenticate_super_admin!
def new
#thecategory = Category.new
#thebrands = Brand.all
render "categories/new"
end
end
In my test
require 'rails_helper'
RSpec.describe CategoriesController, type: :controller do
before(:all) do
#the_super_admin = createLoggedInSuperAdmin
end
context "#new" do
it "instantiates new category" do
get :new
expect(assigns(:thecategory)).to be_a_new(Category)
end
end
end
it keeps on telling me expected nil to be a kind of Model and when I inspect it with pry assigns(:themodel) shows as nil
I haven't been able to find any answers that help me with this situation
Turns out I was an idiot, the createLoggedInSuperAdmin call should have been wrapped in before(:each) instead of before(:all), that was why the authentication was working for the first test but failing on the second one

How to test the AdminController when it has no actions?

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!

How do i get rid of #controller is nil error in my tests

I keep getting
#controller is nil: make sure you set it in your test's setup method.
When I run my tests. Any idea what this means?
when you inherit from ActionController::TestCase it infers the controller name from the test name if they do not match you have to use the setup part of test to set it.
So if you have
class PostsControllerTest < ActionController::TestCase
def test_index
#assert something
end
end
Then #controller is auto instantiated to PostsController, however, if this were not the case and you had a different name you would need a setup as such
class SomeTest < ActionController::TestCase
def setup
#controller = PostController.new
end
end
I was in the process of upgrading to rspec 3 from the beta version on rails 4 and ran into this error. The problem turned out to be that our Controller spec describe statements used symbols instead of strings. Rspec was attempting to instantiate the symbol as the controller but they were in fact 'actions'.
#trys to set #controller = Index.new
describe SomeController do
describe :index do
before do
get :index, format: :json
end
it { expect(response).to be_success}
end
end
#works
describe SomeController do
describe 'index' do
before do
get :index, format: :json
end
it { expect(response).to be_success}
end
end
ErsatzRyan answer is correct, however there is a small typo. Instead of
#controller = PostsController
it should be
#controller = PostsController.new
otherwise you get an error: undefined method `response_body='
Check whether you are ending the do and end properly.
RSpec.describe LeadsController, type: :controller do
# All tests should come here
end
If the names match, and the #controller variable is still nil, try checking for errors in the controller instantiation. For me I had a controller initialize method that had a bug in it. For some reason the controller was just nil in the test, rather than throwing an error when it wasn't instantiated.
Or, you can simply do this:
RSpec.describe PostsControllerTest, :type => :controller do
# ...
end
In Rails 6 looks like it is like this:
tests PostController
https://www.rubydoc.info/docs/rails/4.1.7/ActionController/TestCase#label-Controller+is+automatically+inferred
I encountered this error because I surrounded the controller name in quotes.
# broken
RSpec.describe 'RegistrationsController', type: :controller do
...
end
# works
RSpec.describe RegistrationsController, type: :controller do
...
end

Resources