I wanted to create a module because i needed to execute some methods upon every controller action of my application. Thus, i created a module and used a before_filter to my application controller, so that it runs for every action.
This module is like a controller extension, since it handles redirects and has some methods as well. I need to have a logged in user to check that, but for some reason, the macro that i have for my other controllers is not recognized :
require 'spec_helper'
require 'devise/test_helpers'
describe GameEngine::GameLoop do
login_user
it "should check tavern quest" do
...
end
end
I get that login_user is undefined, though it works for all my controller, where i need it. Why isn't it recognized on the module ?
Related
I have one main rails app, and I have an engine within it, now the engine has a different test, I need to test one controller which is using main app MailerHelper module. How can I access in my spec.
My engine's user controller
module ControlPanel
class UserController < ApplicationController
include MailerHelper
def index
#users = User.all
end
end
end
I am using MailerHelper have many other methods which sent mails. As this is controller is in my engine control panel, following is specification in my engine
require 'rails_helper'
describe ControlPanel::UsersController, type: :controller do
describe '#index' do
it 'should display all users' do
get :index
expect(response).to render_template('index')
end
end
end
The test case is just simple, but as in my engines User controller I am using my main application helper, so it can't take it, I have seen many replies but nothing working, like including in rails_spec and ::MailerHelper etc.
I am trying to follow the new Rails testing convention with this new project I am working on. Therefore I setup unit tests, request tests and feature tests. The issue is that in the request tests, Capybara and the the session information are not supported.
In addition to having the current_user method setup by Devise, my application has another similar method called current_client. For some of my controllers, I need to check whether the current_user is logged in. This works with Devise authenticate_user! called in a before_action. But for some controllers, I also need to check if a client was selected first (for example if you want to add transactions, they need to be tied with the current client being worked on).
So I added another before_action method that checks if a client was also selected. This works well in feature tests with Capybara where I can mimic user loggin in and the user selecting a client to process. However in the request tests, It doesn`t work.
I first test trying to access the endpoint without a user being logged in and the response is not successful (as it should) and it redirects to the sign in page. But then I need to run my tests with a user logged in AND a client selected. Devise helpers provide a sign_in(user) method. However I can't get my current_user method to work and I can't seem to be able to set it up properly. So those tests are failing and redirecting to another page asking the user to select a client.
I have already tried many suggestions I saw. Like trying to stub the current_client method, trying to provide session info to the GET call. I even tried to check the Devise source code to find out how they can simulate the current_user method in Rspec and couldn't really find where the magic happens.
Here is the code
The current_client method looks like this
def current_client
current_client ||= Client.find(session[:client_id]) if session[:client_id]
rescue ActiveRecord::RecordNotFound
current_client = nil
end
This is how it is set once a user selects a client to process
def set_current_client(client)
session[:client_id] = client.id
end
This is the code in my test file
I first create 2 users so that I can test that user 1 cannot access user 2 transactions down the line. (I'm using FactoryBot)
I then create 2 clients (one for each user)
RSpec.describe "Rooms", :type => :request do
let!(:user) {create(:user)}
let!(:user2) {create(:user2)}
let!(:client) {create(:client, user: user)}
let!(:client) {create(:client, user: user2)}
The following works
describe 'User Not Signed In' do
describe 'GET #index' do
subject { get transact_path}
it "returns an unsuccessful response" do
subject
expect(response).to_not be_successful
end
it "redirects to sign in page" do
subject
expect(response).to redirect_to (new_user_session_path)
end
end
The following doesn't. The sign_in(user) method works in the before block and the redirect is not made to the sign in page. However, the response is not successful because the current_client is not set and I have tried to set it in so many ways in the before block without success.
describe 'User Signed In' do
before do
sign_in(user)
end
describe 'GET #index' do
it "returns a successful response" do
get transact_path
expect(response).to be_successful
end
end
end
I understand that the best practices moved away from controller tests because what view rendered or what instance variable assigned doesn't have anything to do with controllers in theory. But in my case, I'm simply trying to test my endpoints and right now I can't because I cannot setup my current_client.
I found a way around it by using a suggested solution by DHH himself.
Instead of trying to stub the current_client variable or try to jury rig something in the gut of ActionDispatch::Cookies, you simply need to do a POST or GET call to whatever controller is responsible for setting my current_client variable.
So for me the solution was to put the following code in a before do block
before do
sign_in(user)
get select_client_path, params: {id: client.id}
end
lib/modules/api.rb:
module Api
require 'net/http'
require 'json'
def send_get_request(url, body)
# some logic
end
end
The controller:
class DashboardController < ApplicationController
include Api
def index
response = send_get_request(_some_, _params_)[:json]
#something = response['something']
end
end
How do I stub the send_get_request method? I tried a feature test:
require 'rails_helper'
describe 'visiting users page'
it 'shows users page' do
visit '/'
allow_any_instance_of(Api).to receive(:send_get_request).with(any_args).and_return({json: {'total_paying' => '3'}})
within('#side-menu') do
click_link 'Users'
end
expect(page).to have_selector('.page-header', text: 'Users')
end
end
but apparently it doesn't work (the test fails because real send_get_request method gets called with params that are incorrect (which they are in tests).
When I tried
expect_any_instance_of(Api).to receive(:send_get_request).once
That expectation passed.
Well, it's the controller that will receive the message. The test, as a client, does not care about how the method is defined, "normally" or mixed-in.
If it were a controller spec, you'd be able to do this:
allow(controller).to receive(:send_get_request).with(...)
As for the feature specs, don't stub anything in there. It's supposed to be the "real" interaction. In feature specs you use capybara (or something) to fill the forms and then check the database (to see if a user got created, etc.)
To shield your feature specs from external api, you can use VCR gem. It basically runs your external query one time, writes the response into a file and then, on subsequent runs, just plays it back, without contacting external api again.
I have a very simple controller:
class ThingsController < ApplicationController
def create
Thing.do_stuff
end
end
…And a very simple spec:
require "rails_helper"
describe ThingsController do
describe "POST #create" do
it "does stuff with things" do
expect(Thing).to receive(:do_stuff)
controller.create # This works
post :create # This does not work
end
end
end
I am not running the direct invocation and the post request at the same time. Invoking the action on the controller directly passes the assertion, but invoking the action through the post method does not. It appears do_stuff is never called on Thing. Why might that be?
I discovered what my issue was.
Invoking the controller directly keeps the spec isolated and ignores things like a before_action in the ApplicationController.
When we start using the post method, it’s really an integrated test and hits things like authentication. I couldn’t hit my controller method because my test user wasn’t signed in.
I have rspec controller test:
describe TestController do
it "test all actions" do
all_controller_actions.each do |a|
expect{get a}.to_not rais_error(SomeError)
end
end
end
How to implement all_controller_actions method?
A better way is to write a different test for each action method in the controller.
If you look at the docs on Rails TestCase class -- the class that controller tests are created from (even rspec just wraps this class), you'll see what I mean:
http://api.rubyonrails.org/classes/ActionController/TestCase.html
The docs say:
Functional tests allow you to test a single controller action per test method.
The intention is that controller tests have a different test method for each action in the controller.
While I prefer to test one by one, your question is doable.
# Must state this variable to be excluded later because MyController has them.
a = ApplicationController.action_methods
m = MyController.action_methods
# Custom methods to exclude
e = %w{"create", "post"}
#test_methods = m - a - e
describe TestController do
it "all GET actions got response" do
#test_methods.each do |t|
expect{get t}.to_not rais_error(SomeError)
end
end
end
You should aim to create a different test for each action of the controller to make tests more expressive and easier to understand. Each action would mostly be located in its own describe blocks, with each meaningful input having its own context block.
For an example:
describe "Users" do
describe "GET user#index" do
context "when the user is logged in" do
it "should render users#index"
end
context "when the user is logged out" do
it "should redirect to the login page"
end
end
end
The example has different authentications for logged in and logged out users, we separated it in different context blocs under the describe "GET user#index" block. You can find a more detailed explanation here.