I'm really struggling with Rspec DSL! I read a lot on SO and across the internet, so I'm posting my particular issue because my feeble mind can't get to the solution.
Have a post method in my controller that updates a user's email. Works fine, but I'm struggling with the spec because all I'm getting are undefined methods for NilClass (even though I've tried stubbing every object and method, etc).
users_controller.rb
def update_user_email
#user = User.find_by_id(params[:id])
new_email = params[:user][:new_email].downcase.strip
user_check = User.find_by_email('new_email')
if user_check.blank?
#user.email = new_email
#user.save
flash[:notice] = "Email updated to #{new_email}"
else
flash[:alert] = "This email is already being used by someone else!"
end
respond_with #user do |format|
format.html { redirect_to admin_user_path(#user) }
format.json { head :no_content }
end
end
Here's the spec I'm trying to write. What test should I be writing, if not this, and what can I do to prevent undefined method on NilClass errors!
users_controller_spec.rb
describe Admin::UsersController do
let!(:user) { FactoryGirl.create(:user, password: 'oldpass', email: 'bar#foo.com') }
...
describe "admin actions for each user" do
it "resets user email" do
post :update_user_email, {user: {new_email: 'foo#bar.com'} }
response.status.should == 200
end
end
...
end
And the error:
Admin::UsersController admin actions for each user resets user email
Failure/Error: post :update_user_email, {user: {new_email: 'foo#bar.com'} }
NoMethodError:
undefined method `email=' for nil:NilClass
The line that is failing is:
#user = User.find_by_id(params[:id)
Since you are not passing the id in during your test the user is not being found, and therefore you are trying to call email= on nil. Here is how you can clean up your controller and test.
class YourController < ApplicationController
before_filter :find_user, only: [:update_user_email]
def update_user_email
new_email = params[:user][:new_email].downcase.strip
user_check = User.where(email: new_email)
if user_check.blank?
#user.email = new_email
#user.save
flash[:notice] = "Email updated to #{new_email}"
else
flash[:alert] = "This email is already being used by someone else!"
end
respond_with #user do |format|
format.html { redirect_to admin_user_path(#user) }
format.json { head :no_content }
end
end
def find_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
flash[:error] = "It looks like that user does not exist"
# redirect or render
end
end
# your test
describe "admin actions for each user" do
it "resets user email" do
post :update_user_email, id: user.id, user: {new_email: 'foo#bar.com'}
response.status.should == 200
end
end
You may also want to consider moving the logic out of the controller and into a service object. That controller method is getting a little long.
The issue is that you also need to pass the id of the User you're wanting to update. The line which is failing is #user.email = new_email, since #user is nil.
To get your test to pass now, you'll need to change your post method to:
post :update_user_email, {id:'bar#foo.com', user: {new_email: 'foo#bar.com'} }
As an aside, it is possible to say that it may be better for you to actually be doing this in the UsersController#update method, in order to maintain RESTful routes. And as for enforcing unique email address - it might be better to do this in the User class with validations.
In your post :update_user_email you're not passing :id... so #user = User.find_by_id... is not finding a user so #user is a nil object.
post :update_user_email, id: user.id, {user: {new_email: 'foo#bar.com'} }
Related
In my Rails 4 app I have this update action:
class UsersController < ApplicationController
...
def update
current_email = #user.email
new_email = user_params[:email].downcase
if #user.update_attributes(user_params)
if current_email != new_email
#user.email = current_email
#user.new_email = new_email.downcase
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
redirect_to edit_user_path(#user)
else
render :edit
end
end
...
end
It basically makes sure that a user cannot simply save any new email address. He will have to confirm it first by clicking on a link in an email we send to him.
This works great, however, for some reason I haven't found a way to test it.
The following RSpec test keeps failing no matter what I do:
it "changes the user's new_email attribute" do
#user = FactoryGirl.create(:user, :email => "john#doe.com")
patch :update, :id => #user, :user => FactoryGirl.attributes_for(:user, :email => "new#email.com")
expect(#user.reload.new_email).to eq("new#email.com")
end
#user.new_email is always nil and the test always fails. What am I missing here?
Re-factoring my update action wouldn't be a problem at all. Maybe there's a better way? Thanks for any help.
I would write the spec like so:
let(:user) { FactoryGirl.create(:user, email: "john#doe.com") }
it "changes the user's new_email attribute" do
expect do
patch :update, id: #user, user: FactoryGirl.attributes_for(:user, email: "new#email.com")
user.reload
end.to change(user, :new_email).from("john#doe.com").to("new#email.com")
end
When it comes to the controller action itself the problem is that the new_email property is never saved to the database, besides that its kind of a mess. You can clean it up by using ActiveRecord::Dirty which tracks attribute changes in the model:
class User < ApplicationRecord
# updates user with attrs but moves a new email to the `new_email`
# column instead
def update_with_email(attrs, &block)
update(attrs) do |record|
if record.email_changed?
record.new_email = record.email.downcase
record.restore_attribute!(:email)
end
# keeps the method signature the same as the normal update
yield record if block_given?
end
end
end
Putting this business logic in the model also lets you test it separatly:
RSpec.describe User, type: :model do
describe "#update_with_email" do
let(:user) { FactoryGirl.create(:user) }
it "does not change the email attribute" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to_not change(user, :email)
end
it "updates the new_email" do
expect do
user.update_with_email(email: ”xxx#example.com”)
user.reload
end.to change(user, :new_email).to('xxx#example.com')
end
end
end
This lets you keep the controller nice and skinny:
def update
if #user.update_with_email(user_params)
if #user.new_email_changed?
#user.send_email_confirmation_email
flash[:success] = "Please click the link we've just sent you to confirm your new email address."
else
flash[:success] = "User updated."
end
# You probably want to redirect the user away from the form instead.
redirect_to edit_user_path(#user)
else
render :edit
end
end
There are many answered questions about this topic but I can't seem to apply the answers to my issue. I'm getting the following error:
ItemsController POST create redirect to show page
Failure/Error: #user = User.find(params[:user_id])
ActiveRecord::RecordNotFound:
Couldn't find User with 'id'=
I know the reason for the error is because the user_id is nil but I can't seem to figure out the reason why. I'm still fairly new to Rails so excuse me if this is a really easy fix.
RSpec Test
require 'rails_helper'
RSpec.describe ItemsController, type: :controller do
describe "POST create" do
it "redirect to show page" do
post :create, user_id: #user, item: { name: "name"}
expect(response).to redirect_to(user_show_path)
end
end
end
Items Controller
class ItemsController < ApplicationController
def create
#user = User.find(params[:user_id])
#item = Item.new(item_params)
#item.user = current_user
if #item.save
#item = Item.update_items(params[:items])
redirect_to current_user, notice: "Item was saved successfully."
else
flash[:error] = "Error creating item. Please try again."
render user_show_path
end
end
private
def item_params
params.require(:item).permit(:name)
end
end
Routes
user_items POST /users/:user_id/items(.:format) items#create
...and I've implemented Devise for the user part. Thanks in advance for the help!
You need to create #user before using it. You can use factory_girl gem or directly do
#user = User.create
post :create, user_id: #user.id, item: { name: "name"}
Hope this works
I am trying to test a controller create method in a rails app using RSpec as shown below:
def create
#user = User.new(user_params)
if #user.save
redirect_to user_path(#user.id)
else
render new_user_path
flash[:error] = "User not saved"
end
end
However if i stub out .new to prevent the test from using Active Record and the User model by forcing it to return true the id of the #user is not set by .save as normal so I cannot test for it redirecting to user_path(#user.id) as #user.id is nil
Here is my initial test for RSpec:
it "creates a user and redirects" do
expect_any_instance_of(User).to receive(:save).and_return(true)
post :create, { user: {name: "John", username: "Johnny98", email: "johnny98#example.com"} }
expect(assigns(:user).name).to eq("John")
expect(response).to redirect_to user_path(assigns(:user))
end
How should I test for this redirect in RSpec.
You should use mocks - https://www.relishapp.com/rspec/rspec-mocks/docs.
user = double("user", id: 1, save: true)
Then you should mock you method with double you've just created
expect(User).to receive(:new).and_return(user)
And then test redirect.
expect(response).to redirect_to user_path(user)
I hope this will help.
I would do it in this way:
it 'should redirect to a user if save returned true' do
#user_instance = double
#user_id = double
allow(User).to receive(:new).and_return(#user_instance)
allow(#user_instance).to receive(:save).and_return(true)
allow(#user_instance).to receive(:id).and_return(#user_id)
post :create, {:user => valid_attributes}
expect(response).to redirect_to(user_path(#user_id))
end
I'm testing an action but I can't make it pass.
My spec:
it 'redirects to the datatables page for authenticated users' do
user = User.create(name: 'fer', email: 'fer#fer.fer', password: 'fer')
session[:user_id] = user.id
get :new
expect(response).to redirect_to datatables_lists_path
end
My code:
def create
user = User.where(email: params[:email]).first
if user && user.authenticate(params[:password] )
session[:user_id] = user.id
redirect_to datatables_lists_path, notice: 'you are in!! :)'
else
flash[:error] = 'it's not working'
redirect_to sign_in_path
end
end
The message I'm getting is:
1) SessionsController redirects to the datatables page for authenticated users
Failure/Error: expect(response).to redirect_to datatables_lists_path
Expected response to be a <redirect>, but was <200>
You're issuing a get :new and yet you're showing us the create action from your controller. Your test isn't testing the create action.
I'm following Ryan Bates railscasts on password reseting. I decided to implement his code through TDD but one of my tests refuses to work. Whenever I run
context "for checking update succeeds" do
before do
post :update, id: "2012-12-03 04:23:13 UTC"
end
it { should assign_to(:user) }
it { should respond_with(:redirect) }
it { should redirect_to(signin_path) }
it { should set_the_flash.to("Password has been reset.")}
end
I get the following error
Failure/Error: post :update, id: "2012-12-03 04:23:13 UTC"
NoMethodError:
undefined method `<' for nil:NilClass
My controller is as follows
def update
#user = User.find_by_password_reset_token!(params[:id])
if #user.password_reset_sent_at < 2.hours.ago
redirect_to new_password_reset_path, flash[:error] = "Password reset has expired"
elsif #user.update_attributes(params[:user])
redirect_to root_url, flash[:success] = "Password has been reset."
else
render :edit
end
end
I have to assume I'm writing my test wrong in some form or fashion. How do I fix this? Note that I'm aware the utc time is off date and based on yesterday's time.
Copied from comment:
Seems like #user.password_reset_sent_at is nil. If it is allowed to be nil, you should check by
if #user.password_reset_sent_at && #user.password_reset_sent_at < 2.hours.ago
If it shouldn't be allowed to, you have a bug somewhere in your model. The test seems fine.