I'm new to RSpec and I'm just wondering how to reuse a context across several actions in a controller. Specifically, I have code like this:
describe "GET index" do
context "when authorized" do
...
end
context "when unauthorized" do
it "denys access"
end
end
describe "GET show" do
context "when authorized" do
...
end
context "when unauthorized" do
it "denys access"
end
end
...
And I'd like to DRY it up a bit. The unauthorized context is the same on every action, how can I reuse it?
Shared examples are your friend:
Create a new file, something like spec/shared/unauthorized.rb and include it in your spec_helper then format it like this:
shared_examples_for "unauthorized" do
context "when unauthorized" do
it "denys access"
end
end
Then in your specs:
include_examples "unauthorized"
Do that in each describe block and you should be golden.
if you use popular gem Devise, you can reuse devise mapping like this:
require "spec_helper"
describe Worksheet::CompanyController do
login_user_admin #<= this macros on /spec/support/controller_macros.rb
describe '#create' do
it 'admin login and create worksheet' do
post :create, worksheet_company: attributes_for(:"Worksheet::Company")
expect(response.status).to eq(302)
expect(response).to redirect_to(admin_root_path)
end
end
create and login admin_user
spec/support/controller_macros.rb
module ControllerMacros
def login_user_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user_admin]
user_admin = FactoryGirl.create(:user_admin)
user_admin.confirm!
sign_in user_admin
end
end
end
on spec/spec_helper.rb:
RSpec.configure do |config|
...
config.include Devise::TestHelpers, type: :controller
config.extend ControllerMacros, type: :controller
...
end
Related
I can't understand why my second test does not work.
This works perfectly fine:
require 'rails_helper'
require 'spec_helper'
RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers
end
RSpec.describe 'GET /users/:id', type: :request do
before(:all) do
#user = User.find_by(email: "user#dev.test")
sign_in #user
end
it "returns a user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
end
but, if I add a second request in the same test like this:
require 'rails_helper'
require 'spec_helper'
RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers
end
RSpec.describe 'GET /users/:id', type: :request do
before(:all) do
#user = User.find_by(email: "user#dev.test")
sign_in #user
end
it "returns a user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
it "returns another user object" do
get "/users/#{#user.id}.json"
expect(response.status).to eq 200
expect(response).to have_http_status(:success)
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(JSON.parse(response.body)["successful"]).to eql(true)
end
end
the test fails with error:
ActionController::RoutingError: No route matches [GET] "/users/2.json"
as you can see both tests are the same, but for some reason the second test always fail.
I think it is because there is no second User in Test Environment.
Add second user like this
before(:all) do
#user_second = User.create(user_params)
...
end
But best practice is to use gems like factory-bot and 'database-cleaner'. Also try to use 'byebug'.
I'm having an issue logging in a user with Devise/Rspec to hit a route for testing. I'm used the support/controller_macros module as outlined by devise, but whenever I try using
login_user in any part of my test I get the error: before is not available from within an example (e.g. an it block) or from constructs that run in the scope of an example (e.g. before, let, etc). It is only available on an example group (e.g. a describe or context block).
I've tried a moving things around, making sure all of my requires are set up correctly, etc.
My test:
require "rails_helper"
RSpec.describe BallotsController, type: :controller do
describe "index" do
it "renders" do
login_user
ballots_path
expect(response).to be_success
expect(response).to render_template("index")
end
end
end
(I've tried adding login_user inside the describe block, and the upper block as well)
My controller_macros:
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user_confirmed]
user = FactoryBot.create(:user_confirmed)
sign_in user
end
end
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
user = FactoryBot.create(:admin)
sign_in user
end
end
end
My spec helper:
require "rails_helper"
require_relative "support/controller_macros"
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.include ControllerMacros, :type => :controller
config.include Devise::Test::ControllerHelpers, :type => :controller
config.example_status_persistence_file_path = "spec/examples.txt"
config.disable_monkey_patching!
if config.files_to_run.one?
config.default_formatter = "doc"
end
config.profile_examples = 10
config.order = :random
Kernel.srand config.seed
end
I expect it to log a user in, and for the controller to correctly hit the index route. Thank you!
Only issue in the code(to resolve the error, without digging into the debate of if this method should be used or not) is that you have login user within the it block, which can't be because it is calling before(:each).
If you see the device documentation, you will also see that it does not have this call in it block, but rather outside of it block in a describe block. Which applies this call to all it blocks in that describe block.
Your code willcbecome:
RSpec.describe BallotsController, type: :controller do
describe "index" do
login_user
it "renders" do
ballots_path
expect(response).to be_success
expect(response).to render_template("index")
end
end
end
The way I prefer:
In your controller_macros, replace the login_user with:
def login_user(user)
#request.env["devise.mapping"] = Devise.mappings[:user_confirmed]
sign_in user
end
Now, wherever you want to login user, you can do it something like:
RSpec.describe BallotsController, type: :controller do
describe "index" do
let(:user) { FactoryBot.create(:user) }
it "renders" do
login_user(user)
ballots_path
expect(response).to be_success
expect(response).to render_template("index")
end
end
# OR
describe "index" do
let(:user) { FactoryBot.create(:user) }
before(:each) do
login_user(user)
end
it "renders" do
ballots_path
expect(response).to be_success
expect(response).to render_template("index")
end
end
end
In controller current_user exists and is signed in, but request is unauthorized. Why is that?
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryBot.create(:user, company: FactoryBot.create(:company))
sign_in user
end
end
end
rspec_helper.rb:
require 'spec_helper'
require 'devise'
require 'support/controller_macros'
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::ControllerHelpers, type: :controller
config.extend ControllerMacros, type: :controller
end
Users test:
require 'rails_helper'
RSpec.describe Api::V1::UsersController do
include Devise::Test::ControllerHelpers
describe "GET /api/v1/users" do
login_user
it "should get list of users" do
get 'index'
expect(response).to have_http_status(:success)
end
end
end
Error:
Failure/Error: expect(response).to have_http_status(:success)
This tests are passing and working fine:
it "should have a current_user" do
expect(subject.current_user).to_not eq(nil)
end
it "should be signed in" do
expect(subject.user_signed_in?).to be true
end
Try the below code:
require 'rails_helper'
RSpec.describe Api::V1::UsersController do
include Devise::Test::ControllerHelpers
describe "GET /api/v1/users" do
it "should get list of users" do
login_user
get 'index'
expect(response).to have_http_status(:success)
end
end
end
describe "action name or route " do
...omitted for brevity
it "should get list of users" do
get 'index', params: {}, headers: {...some_type_of_headers e.g BASIC, JWT}
expect(response).to have_http_status(:success)
end
end
I'm trying to add a function to allow for quick testing of redirects for unauthenticated users. Here's what I have so far:
def unauthenticated_redirects_to redirect_path #yeild
context "when not signed in" do
it "redirects to #{redirect_path}" do
yield
expect(response).to redirect_to redirect_path
end
end
end
describe SomeController do
describe 'GET #show' do
unauthenticated_redirects_to('/some_path') { get :show }
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #whatever' do
unauthenticated_redirects_to('/some_other_path') { get :whatever }
end
end
This doesn't work, however, since the scope and context of the primary describe block is not available to the block passed to unauthenticated_redirects_to. This reasonably leads to the error: undefined method `get' for RSpec::Core::ExampleGroup::Nested_1::Nested_2:Class.
Is there a way around this or is there a cleaner way to accomplish something similar which I should consider?
Here's an approach using shared examples which triggers the example based on shared metadata (:auth => true in this case) and which parses the example group description to pick up some key parameters.
require 'spec_helper'
class SomeController < ApplicationController
end
describe SomeController, type: :controller do
shared_examples_for :auth => true do
it "redirects when not signed in" do
metadata = example.metadata
description = metadata[:example_group][:description_args][0]
redirect_path = metadata[:failure_redirect]
http_verb = description.split[0].downcase.to_s
controller_method = description.match(/#(.*)$/)[1]
send(http_verb, controller_method)
expect(response).to redirect_to redirect_path
end
end
describe 'GET #show', :auth => true, :failure_redirect => '/some_path' do
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #whatever', :auth => true, :failure_redirect => '/some_other_path' do
end
end
For completeness, here's another shared examples approach, this time using a block parameter with a before call which avoids the original scope problem:
require 'spec_helper'
class SomeController < ApplicationController
end
describe SomeController, type: :controller do
shared_examples_for 'auth ops' do
it "redirects when not signed in" do
expect(response).to redirect_to redirect_path
end
end
describe 'GET #show' do
it_behaves_like 'auth ops' do
let(:redirect_path) {'/some_path'}
before {get :show}
end
context "when signed in" do
# One thing...
# Another thing...
end
end
describe 'GET #new' do
it_behaves_like 'auth ops' do
let(:redirect_path) {'/some_other_path'}
before {get :whatever}
end
end
end
Have a look at rspec shared example.
Using shared_examples_for seemed like overkill given that I was only concerned with a single example. Furthermore, it_behaves_like("unauthenticated redirects to", '/some_other_path', Proc.new{ get :whatever}) seems unnecessarily verbose. The trick is to use #send() to maintain the proper scope.
def unauthenticated_redirects_to path, method_action
context "when not signed in" do
it "redirects to #{path} for #{method_action}" do
send(method_action.first[0], method_action.first[1])
expect(response).to redirect_to path
end
end
end
describe 'GET #new' do
unauthenticated_redirects_to '/path', :get => :new
end
So I have read how to solve this problem:
RSpec Test of Custom Devise Session Controller Fails with AbstractController::ActionNotFound
and
http://lostincode.net/blog/testing-devise-controllers
But under which file do I add these changes is my problem:
Under the rspec folder for my
registrations_controller
I tried this
before :each do
request.env['devise.mapping'] = Devise.mappings[:user]
end
require 'spec_helper'
describe RegistrationsController do
describe "GET 'edit'" do
it "should be successful" do
get 'edit'
response.should be_success
end
end
end
Which didn't work, any help with the specific files to change to make this work would be greatly appreciated.
EDIT
So I also tried -
https://github.com/plataformatec/devise/wiki/How-To:-Controllers-and-Views-tests-with-Rails-3-(and-rspec)
so I made a folder with spec/support and made a file called controllers_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
And my registrations_controller is now this:
require 'spec_helper'
describe RegistrationsController do
describe "GET 'edit'" do
before :each do
request.env['devise.mapping'] = Devise.mappings[:user]
end
it "should be successful" do
get 'edit'
response.should be_success
end
end
end
I have other controllers in rspec do I need to change every single one? Or I'm confused on where to make the changes.
Just take the first version you tried, but move the before block inside the first describe block like this:
require 'spec_helper'
describe RegistrationsController do
before :each do
request.env['devise.mapping'] = Devise.mappings[:user]
end
describe "GET 'edit'" do
it "should be successful" do
get 'edit'
response.should be_success
end
end
end