need to reload variable after updating attributes in rails - ruby-on-rails

WHen I'm testing in rspec or cucumber, everytime I update an attribute, I have to "reload" the variable again:
#answer.update_attributes(notified: false)
#answer = Answer.find(#answer)
Otherwise #answer doesn't update. This seems inefficient. Is this right? Am I doing something wrong or this is the way it's suposed to work?
Edit: complete spec
describe "notify method" do
before (:each) do
#user = FactoryGirl.create(:user)
friend = generate_friend_for_user(#user)
#answer = FactoryGirl.create(:answer, user: friend, contact: #user.service)
#answer.update_attributes(notified: false)
#answer = Answer.find(#answer)
end
it "should send an email and update last_email field" do
#user.update_attributes(last_email: 25.hours.ago)
#answer.notify.should == true
#user = User.find(#user)
#user.last_email.should > 1.minute.ago
end

I have tried to replicate the issue, and I can see that using ActiveRecord#update reloads the record in memory, while ActiveRecord#update_attributes does not. Is there a specific reason why you are using ActiveRecord#update_attributes? The method is also deprecated in Rails 6.

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 Controller test, testing a post action

Right now I have a subscriber controller that creates a subscriber but that is not what I want to test. I also have a method in the controller that add 1 to the visit attribute on the Subscriber(I'll post the code) that is the method I want to test but I'm not sure how? I'm new to rails and Rspec so I'm having trouble grasping the concepts. I'll post my test and controller for clarity.
CONTROLLER:
def search
#subscriber = Subscriber.new
end
def visit
#subscriber = Subscriber.find_by_phone_number(params[:phone_number])
if #subscriber
#subscriber.visit =+ 1
#subscriber.save
flash[:notice] = "thanks"
redirect_to subscribers_search_path(:subscriber)
else
render "search"
end
end
TEST
it "adds 1 to the visit attribute" do
sign_in(user)
subscriber = FactoryGirl.create(:subscriber)
visits_before = subscriber.visit
post :create, phone_number: subscriber.phone_number
subscriber.reload
expect(subscriber.visit).to eq(visits_before)
end
ERROR MESSAGE:
As you can see that is the method I want to test. The current test in place does not work but I thought it might help to show what I'm thinking. Hopefully this is enough info, let me know if you want to see anything else?
I think you could do something like this:
it 'adds 1 to the visit attribute' do
# I'm assuming you need this, and you are creating the user before
sign_in(user)
# I'm assuming your factory is correct
subscriber = FactoryGirl.create(:subscriber)
visits_before = subscriber.visit
post :create, subscriber: { phone_number: subscriber.phone_number }
subscriber.reload
expect(subscriber.visit).to eq(visits_before)
end
Since you are checking subscriber.visits you should change Subscriber to subscriber:
expect { post :create, :subscriber => subscriber }.to change(subscriber, :visit).by(1)
visits is a method of an instance, not a class method.
I think you're testing the wrong method. You've already stated that your create action works, so no need to test it here. Unit tests are all about isolating the method under test.
Your test as it is written is testing that post :create does something. If you want to test that your visit method does something, you'd need to do something like this:
describe "#GET visit" do
before { allow(Subscriber).to receive(:find).and_return(subscriber) }
let(:subscriber) { FactoryGirl.create(:subscriber) }
it "adds one to the visit attribute" do
sign_in(user)
expect { get :visit }.to change(subscriber, :visit).by(1)
end
end

How to test a controller with steps to use some action

In my system, I have a user that have one company that have multiple accounts.
User sign in system using Devise, and have a virtual attribute called selected_company that was setted in CompaniesController.
I want to make multiple tests in AccountsController with this scenario.
I have this code to sign_in user, this code works well:
before :each do
#user = create(:user)
#user.confirm!
sign_in #user
end
But I must to have a specific context that I tried to code as:
context 'when user already selected a company' do
before :each do
#company = create(:company)
#account = create(:account)
#company.accounts << #account
#user.selected_company = #company
end
it "GET #index must assings #accounts with selected_company.accounts" do
get :index
expect(assigns(accounts)).to match_array [#account]
end
end
But this code won't work, when I run it I got this error:
undefined method `accounts' for nil:NilClass
My AccountsController#index have only this code:
def index
#accounts = current_user.selected_company.accounts
end
I'm new in rspec and TDD and I have some time to test everything I want, and I want to test everything to practice rspec.
I don't know if this is the best way to test this things, so I'm open to suggestions.
Replace with:
expect(assigns(:accounts)).to match_array [#accounts]
Note, :accounts instead of just account.
Also, as I see it, you don't have #accounts in your spec. Please declare that, too. :)
Probably you are not saving selected_company and when you call this on your controller it returns nil.
Try save #user.save after set selected_company:
context 'when user already selected a company' do
before :each do
#company = create(:company)
#account = create(:account)
#company.accounts << #account
#user.selected_company = #company
#user.save
end
it "GET #index must assings #accounts with selected_company.accounts" do
get :index
expect(assigns(accounts)).to match_array [#account]
end
end
Hope to help you.
Finaly, I found the problem!
I changed the before statement to:
before :each do
#company = create(:company)
#account = create(:account)
#company.accounts << #account
controller.current_user.selected_company = #company
end
And changed assigns(accounts) to assings(:accounts) (with symbol) in expect method.

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.

Why doesn't devise related spec work?

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

Resources