Why doesn't devise related spec work? - ruby-on-rails

First, let me say that login works correctly. The user is logged in for sure. I'm also certain that the post is happening properly (checked the messages and flushes, so i'm certain). And the real action of incrementing, as the test describes, works fine. Only the test fails.
But in this rspec below :
it "should increase the strength ability by one point and also update the strength_points by one if strength is the trained ability" do
#user.str = 10
#user.str_points = 0
post :train_ability, :ability => 'str'
flash[:error].should be_nil
#user.str_points.should == 1
#user.str.should == 11
end
The str and str_points shoulds fail. I'm actually using a login_user function in my macros(as specified in devise), like :
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = :user
#user = Factory.create(:user)
sign_in #user
end
end
end
I'm sure that #user is indeed the current_user, but it seems that any attribute changes do not really happen to #user inside the spec(:user is a factory i created).
Why doesn't this work ? :/

First of all you haven't saved the #user before posting to :train_ability.
There's also a slim chance your #user may be cached after that so reloading it before your assertions could be necessary.
Try changing your spec to the following
it "should increase the strength ability by one point and also update the strength_points by one if strength is the trained ability" do
#user.str = 10
#user.str_points = 0
#user.save! # save the #user object so str is 10 and str_points are 0
post :train_ability, :ability => 'str'
flash[:error].should be_nil
#user.reload # reload the user in case str and str_points are cached
#user.str_points.should == 1
#user.str.should == 11
end

Related

Send variable to view in request spec

I'm working on my first complex test, and I need some help.
I need to create a user, sign him in, and fill a form as him. The page raises an error: undefined method first_name for nil:nilClass
Both functions are pretty easy, here is the test:
it "simulates creator onboard" do
#user = FactoryGirl.create(:user)
puts #user.onboard_token.size
visit onboard_path(:token => #user.onboard_token)
puts #user.nil?
expect(#user.active).to eq(false)
click_on('step-forward')
find('input[name="user[password"]').set "12345678"
find('input[name="user[password_confirmation"]').set "12346578"
find('input[name="commit"]').click
expect(response).to redirect_to(root_path)
expect(#user.active).to eq(true)
end
And here is the controller function:
def onboard
authorize(:user, :onboard?)
if params[:token].present?
if params[:token] != nil && params[:token].size == 40 && !User.where(onboard_token: params[:token]).empty?
#user = User.find_by_onboard_token(params[:token])
end
if !#user.nil?
sign_in(:user, #user)
#brand = #user.profile
sign_out(:user)
end
else
sign_out(:user)
redirect_to root_path
end
end
For some reason, it appears that the #user variable is not passed to the view. I made sure it is not nil, made sure to use the correct capybara functions, but still, #user is nil in view.
Did I miss something ?
Since you're using Selenium with Capybara this test has a number of issues
Objects created in the test thread aren't visible in the app thread if you're using transactional testing - https://github.com/jnicklas/capybara#transactions-and-database-setup
Actions in Capybara are asynchronous so trying to test values on an object immediately after triggering the action (without some kind of expecation on displayed content to make sure the action has completed) is going to be flaky
You can't use visit and 'response' at the same time
An object that is present? can't be nil
If there is a token but no user matches it your code with still attempt to render the view but with no #user and no #brand
To solve these you need to set up something like DatabaseCleaner and make sure you're using truncation or deletion strategy for tests using the selenium driver. That will allow objects created in the test thread to be visible in the app (in your case the user). Then your test should be something more like
it "simulates creator onboard" do
#user = FactoryGirl.create(:user)
visit onboard_path(:token => #user.onboard_token)
#You need to expect for something that should be on the screen before checking user.active - since this can occur before the visit has actually done anything
#expect(#user.reload.active).to eq(false)
click_on('step-forward')
find('input[name="user[password"]').set "12345678"
find('input[name="user[password_confirmation"]').set "12346578"
find('input[name="commit"]').click
expect(page).to have_current_path(root_path)
expect(#user.reload.active).to eq(true)
end
with a controller more like
def onboard
authorize(:user, :onboard?)
if params[:token].present? && params[:token].size == 40
#user = User.find_by_onboard_token(params[:token])
if !#user.nil?
sign_in(:user, #user)
#brand = #user.profile
sign_out(:user)
else
# do something here because it means the token was invalid
end
else
sign_out(:user)
redirect_to root_path
end

RSpec with multi tenancy - Why is this simple test failing?

What I'm doing
I recently implemented multi-tenancy (using scopes) following Multitenancy with Scopes (subscription required) as a guide. NOTE: I am using the dreaded "default_scope" for tenant scoping (as shown in Ryan's Railscast). Everything is working in browser just fine, but many (not all) of my tests are failing and I can't figure out why.
I built authentication from scratch (based on this Railscast: Authentication from Scratch (revised) - subscription required) and using an auth_token for "Remember me" functionality (based on this Railscast: Remember Me & Reset Password).
My question
Why is this test failing, and why do the two workarounds work? I've been stumped for a couple days now and just can't figure it out.
What I think is happening
I'm calling the Jobs#create action, and the Job.count is reducing by 1 instead of increasing by 1. I think what's happening is the job is being created, then the app is losing the 'tenant' assignment (tenant is dropping to nil), and the test is counting Jobs for the wrong tenant.
What's odd is that it's expecting "1" and getting "-1" (and not "0"), which implies it's getting a count (note that there's already a 'seed' job created in the before block, so it's probably counting "1" before calling #create), calling the create action (which should increase the count by 1 to 2 total), then losing the tenant and switching to a nil tenant where there are 0 jobs. So it:
Counts 1 (seed job)
Creates a job
Loses the tenant
Counts 0 jobs in the new (probably nil) tenant
...resulting in a -1 change in the Job.count.
You can see below that I've semi-confirmed this by adding ".unscoped" to my Job.count line in the test. This implies that the expected number of jobs is there, but the jobs just aren't in the tenant the app is testing under.
What I don't understand is how it's losing the tenant.
Code
I've tried to grab the relevant parts of my code, and I've created a dedicated single-test spec to make this as easy to dissect as possible. If I can do anything else to make this easy on possible answerers, just let me know what to do!
# application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
around_filter :scope_current_tenant
private
def current_user
#current_user ||= User.unscoped.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
end
helper_method :current_user
def current_tenant
#current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
helper_method :current_tenant
def update_current_tenant
Tenant.current_id = current_tenant.id if current_tenant
end
helper_method :set_current_tenant
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil
end
end
# sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.unscoped.authenticate(params[:session][:email], params[:session][:password])
if user && user.active? && user.active_tenants.any?
if params[:remember_me]
cookies.permanent[:auth_token] = user.auth_token
else
cookies[:auth_token] = user.auth_token
end
if !user.default_tenant_id.nil? && (default_tenant = Tenant.find(user.default_tenant_id)) && default_tenant.active
# The user has a default tenant set, and that tenant is active
session[:tenant_id] = default_tenant.id
else
# The user doesn't have a default
session[:tenant_id] = user.active_tenants.first.id
end
redirect_back_or root_path
else
flash.now[:error] = "Invalid email/password combination."
#title = "Sign in"
render 'new'
end
end
def destroy
cookies.delete(:auth_token)
session[:tenant_id] = nil
redirect_to root_path
end
end
# jobs_controller.rb
class JobsController < ApplicationController
before_filter :authenticate_admin
# POST /jobs
# POST /jobs.json
def create
#job = Job.new(params[:job])
#job.creator = current_user
respond_to do |format|
if #job.save
format.html { redirect_to #job, notice: 'Job successfully created.' }
format.json { render json: #job, status: :created, location: #job }
else
flash.now[:error] = 'There was a problem creating the Job.'
format.html { render action: "new" }
format.json { render json: #job.errors, status: :unprocessable_entity }
end
end
end
end
# job.rb
class Job < ActiveRecord::Base
has_ancestry
default_scope { where(tenant_id: Tenant.current_id) }
.
.
.
end
# sessions_helper.rb
module SessionsHelper
require 'bcrypt'
def authenticate_admin
deny_access unless admin_signed_in?
end
def deny_access
store_location
redirect_to signin_path, :notice => "Please sign in to access this page."
end
private
def store_location
session[:return_to] = request.fullpath
end
end
# spec_test_helper.rb
module SpecTestHelper
def test_sign_in(user)
request.cookies[:auth_token] = user.auth_token
session[:tenant_id] = user.default_tenant_id
current_user = user
#current_user = user
end
def current_tenant
#current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
end
# test_jobs_controller_spec.rb
require 'spec_helper'
describe JobsController do
before do
# This is all just setup to support requirements that the admin is an "Admin" (role)
# That there's a tenant for him to use
# That there are some workdays - a basic requirement for the app - jobs, checklist
# All of this is to satisfy assocations and
#role = FactoryGirl.create(:role)
#role.name = "Admin"
#role.save
#tenant1 = FactoryGirl.create(:tenant)
#tenant2 = FactoryGirl.create(:tenant)
#tenant3 = FactoryGirl.create(:tenant)
Tenant.current_id = #tenant1.id
#user = FactoryGirl.create(:user)
#workday1 = FactoryGirl.create(:workday)
#workday1.name = Time.now.to_date.strftime("%A")
#workday1.save
#checklist1 = FactoryGirl.create(:checklist)
#job = FactoryGirl.create(:job)
#checklist1.jobs << #job
#workday1.checklists << #checklist1
#admin1 = FactoryGirl.create(:user)
#admin1.tenants << #tenant1
#admin1.roles << #role
#admin1.default_tenant_id = #tenant1.id
#admin1.pin = ""
#admin1.save!
# This is above in the spec_test_helper.rb code
test_sign_in(#admin1)
end
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{ # <-- This is line 33 that's mentioned in the failure below
post :create, job: FactoryGirl.attributes_for(:job)
# This will pass if I change the below to Job.unscoped
# OR it will pass if I add Tenant.current_id = #tenant1.id right here.
# But I shouldn't need to do either of those because
# The tenant should be set by the around_filter in application_controller.rb
# And the default_scope for Job should handle scoping
}.to change(Job,:count).by(1)
end
end
end
end
Here is the failure from rspec:
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66481 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job
If I add some 'puts' lines to see who the current_tenant is directly and by inspecting the session hash, I see the same tenant ID all the way:
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{
puts current_tenant.id.to_s
puts session[:tenant_id]
post :create, job: FactoryGirl.attributes_for(:job)
puts current_tenant.id.to_s
puts session[:tenant_id]
}.to change(Job,:count).by(1)
end
end
end
Yields...
87
87
87
87
F
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66581 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job
I think it's not that RSpec is ignoring the default scope but it's reset in the ApplicationController in the around filter by setting the current user to nil.
I encountered this issue with assigns(...) and it happened because the relation is actually resolved when you're evaluating assigns. I think this may also be the case with the expectation in your case.
UPDATE: In my situation, the cleanest solution I could find (though I still hate it) is to let the default scope leak through by not setting the current user to nil in test environment.
In your case this would amount to:
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil unless Rails.env == 'test'
end
I haven't tested it with your code but maybe this will help.
I managed to get my tests to pass, although I'm still not sure why they were failing to begin with. Here's what I did:
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{ # <-- This is line 33 that's mentioned in the failure below
post :create, job: FactoryGirl.attributes_for(:job)
}.to change(Job.where(tenant_id: #tenant1.id),:count).by(1)
end
end
end
I changed:
change(Job,:count).by(1)
...to:
change(Job.where(tenant_id: #tenant1.id),:count).by(1)
NOTE: #tenant1 is the logged-in admin's tenant.
I assumed default_scopes would be applied in RSpec, but it seems they aren't (or at least not in the ":change" portion of an "expect" block). In this case, the default_scope for Job is:
default_scope { where(tenant_id: Tenant.current_id) }
In fact, if I change that line to:
change(Job.where(tenant_id: Tenant.current_id),:count).by(1)
...it will also pass. So if I explicitly mimic the default_scope for Job within the spec, it'll pass. This seems like confirmation that RSpec is ignoring my default_scope on Jobs.
In a way, I think my new test is a better way to make sure tenant data stays segregated because I'm explicitly checking counts within a particular tenant rather than implicitly checking the counts for a tenant (by assuming the count is in the "current tenant").
I'm marking my answer is correct because it's the only answer, and if someone else encounters this, I think my answer will help them get past the issue. That said, I really haven't answered my original question regarding why the test was failing. If anyone has any insight into why RSpec seems to be ignoring default_scope in "expect" blocks, that might help making this question useful for others.
I have the same issue of you guys. I didn't resolve in a way that makes me comfortable but is still better than verifying your RAILS_ENV. Take this example.
it "saves person" do
expect {
some_post_action
}.to change(Person, :count).by(1)
end
Every time i try to save the count method makes a select like:
"select count(*) from persons where tenant_id is null"
I manage to resolve this issue by setting Person.unscoped in the change method i changed this:
}.to change(Person, :count).by(1)
to this:
}.to change(Person.unscoped, :count).by(1)
It's not the best solution but i'm still trying to find a way to get around the default_scope.

Set current_user in test

I have a test that looks like this:
test "should get create" do
current_user = FactoryGirl.build(:user, email: 'not_saved_email#example.com')
assert_difference('Inquiry.count') do
post :create, FactoryGirl.build(:inquiry)
end
assert_not_nil assigns(:inquiry)
assert_response :redirect
end
That's testing this part of the controller:
def create
#inquiry = Inquiry.new(params[:inquiry])
#inquiry.user_id = current_user.id
if #inquiry.save
flash[:success] = "Inquiry Saved"
redirect_to root_path
else
render 'new'
end
end
and the factory:
FactoryGirl.define do
factory :inquiry do
product_id 2
description 'I have a question about....'
end
end
but I keep getting errors in my tests:
1) Error:
test_should_get_create(InquiriesControllerTest):
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
What am I doing wrong? I need to set the current_user, and I believe I am in the test, but obviously, that's not working.
You didn't create current_user. It was initialized only in test block.
There are two differents ways to do it:
First, use devise test helpers. Something like that
let(:curr_user) { FactoryGirl.create(:user, ...attrs...) }
sign_in curr_user
devise doc
Second, you can stub current_user method in your controllers for test env
controller.stub(current_user: FactroryGirl.create(:user, ...attrs...))
And you should use FactoryGirld.create(...) instead of FactoryGirl.build(...), because you factory objects have to be persisted.(be saved in db and has id attribute not nil)
There are several things which come to mind:
FactoryGirl.build(:user, ...) returns unsaved instance of a user. I'd suggest to use Factory.create instead of it, because with unsaved instance there's no id and there's no way for (usually session based) current_user getter to load it from database. If you're using Devise, you should "sign in" user after creating it. This includes saving record in DB and putting reference to it into session. See devise wiki
Also, passing ActiveRecord object to create action like this looks weird to me:
post :create, FactoryGirl.build(:inquiry)
Maybe there's some rails magic in play which recognizes your intent, but I'd suggest doing it explicitly:
post :create, :inquiry => FactoryGirl.build(:inquiry).attributes
or better yet, decouple it from factory (DRY and aesthetic principles in test code differ from application code):
post :create, :inquiry => {product_id: '2', description: 'I have a question about....'}
This references product with id = 2, unless your DB doesn't have FK reference constraints, product instance may need to be present in DB before action fires.

undefined method `stringify_keys' while using Factory Girl

I have the following block of code in my User_spec.rb:
#user = { username:'newuser',
email:'new#user.com',
fname:'new',
lname:'user',
password:'userpw',
password_confirmation:'userpw'}
for creating a using using these attributes. However while I moved all these attributes to Factories.rb:
require 'factory_girl'
Factory.define :user do |u|
u.username 'newuser'
u.email 'new#user.com'
u.fname 'new'
u.lname 'user'
u.password 'newuserpw'
u.password_confirmation 'newuserpw'
end
and replace the line in user_spec.rb with:
#user = Factory(:user)
all my tests that related to the User model failed(such as tests for email, password, username etc), all were giving me
"undefined method `stringify_keys' for…"
the new user object
I had a similar problem, and it was because I was passing a FactoryGirl object to the ActiveRecord create/new method (whoops!). It looks like you are doing the same thing here.
The first/top #user you have listed is a hash of values, but the second/bottom #user is an instance of your User ojbect (built by FactoryGirl on the fly).
If you are calling something like this in your specs:
user = User.new(#user)
The first (hashed) version of #user will work, but the second (objectified) version will not work (and throw you the 'stringify_keys' error). To use the second version of #user properly, you should have this in your specs:
user = Factory(:user)
Hope that helps.
We need to see an example of a failing test to diagnose, but here is one thing that can cause it – sending an object when attributes are required. I once fixed one of my failing tests by changing:
post :create, organization: #organization
to
post :create, organization: #organization.attributes
#rowanu Answered your question, but let me layout my example too for future reference:
What was failing in my case was:
#user = User.new user_attr
#user.bookings_build(Booking.new booking_attr)
Note that I am trying to build with a booking instance and not hash of attributes
The working example:
user_attr_hash = FactoryGirl.attributes_for(:user)
booking_attr_hash = FactoryGirl.attributes_for(:booking)
#user = User.new user_attr_hash
#user.bookings.build(booking_attr_hash)
And in spec/factories/domain_factory.rb I have
FactoryGirl.define do
factory :user do
# DEFAULT USER...
password "123123123"
email "factory_girl#aaa.aaa"
# there rest of attributes set...
end
factory :booking do
start_date Date.today
end_date Date.today+3
# the rest of attributes
end
end

Ruby on Rails 2.3.8: Testing: How do I set up an instance variable to use throughout my tests?

Lets say I have some data that remanis the same throughout all of my tests, for forever and eternity. I create this data in setup. I store the data to #instance_var. But when I call #instance_var.attribute in any test, I get the following error:
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
I know my instance variable isn't null, cause after it is set, I can do a puts #instance_var.inspect on it...
Any ideas?
EDIT:
setup do
user = Factory(:user)
account = Factory(:account)
set_user(user)
set_account(account)
puts "||||||||||||||||||||||||||||||||||||||||||||||" #this proves that the instance vars are not null
puts "| object_test.rb |"
puts "| #{#user.name} "
puts "| #{#account.name} "
puts "||||||||||||||||||||||||||||||||||||||||||||||"
end
A failing test (with the error above)
test "for detection of first content with multiple contents" do
object = Factory(:object, :user_id => #user.id, :account_id => #account.id)
... #the rest of this test isn't important, as it is the above line, on #user, where the nil.id error occers
in test_helper.rb
def set_user(user)
#user = user
end
def set_account(account)
#account = account
end
I don't really think I need these two methods, as when I define the #instance variable in setup, I get teh same result
in test_helper.rb there are some constants set fore ActiveSupport::TestCase:
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
fixtures :all
disabling these did nothing. =(
Have you tried
setup do
#user = Factory(:user)
#account = Factory(:account)
end
Normally, if you set the instance variables in the setup block, they should be available to all your tests. (You might be having an issue with scopes.)
My solution was to make a shared class, shared_test.rb
require 'test_helper'
class SharedTest
def self.initialize_testing_data
self.reset_the_database
self.set_up_user_and_account
# make sure our user and account got created
puts "|||||||||||||||||||||||||||||||||||||||||||||"
puts "| The user and account "
puts "| we'll be testing with:"
puts "| #{#user.name}"
puts "| #{#user.account.name}"
puts "|||||||||||||||||||||||||||||||||||||||||||||"
end
def self.reset_the_database
#clear the database and reset it
call_rake("db:test:prepare")
call_rake("db:bootstrap RAILS_ENV=test")
end
def self.set_up_user_and_account
#set up our user for doing all our tests (this person is very busy)
#user = Factory(:user)
#account = Factory(:account)
#user.account = #account
#user.save
end
end
so then at the top of every test file that needs user and account to stay the same between all the tests, you just do
require 'shared_test.rb'
and methods are called like
SharedTest.initialize_testing_data

Resources