Can anyone see why my put spec for my controller wont pass?
Here is what my update action in my controller looks like:
def update
#user = User.find(current_user.id)
respond_to do |format|
if #user.update_attributes(permitted_update_params)
format.html { redirect_to new_topup_path, notice: 'Billing address was succesfully updated' }
format.json { respond_with_bip(#user) }
else
format.html { render action: "edit" }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
And my spec looks like this:
context "given a user who wants to update their billing address" do
let!(:user) { create(:user, billing_address: "My initial address") }
before(:each) do
allow(controller).to receive(:current_user) {:user}
patch :update, { user: { :billing_address => "My Second address" } }
end
it "should update the users billing address" do
expect(user.billing_address).to eq("My Second address")
end
end
My spec displays this message:
Failure/Error: expect(user.billing_address).to eq("My Second address")
expected: "My Second address"
got: "My initial address"
You likely need to reload the user instance in your test. The database has been updated, but the user instance won't update itself to reflect that.
expect(user.reload.billing_address).to eq("My Second address")
There are also other problems with your code, like this line:
allow(controller).to receive(:current_user) {:user}
You've defined a user with let(:user), which means you now have a user variable available to your spec - note user, not :user!
You should reload your user before expectation:
before(:each) do
allow(controller).to receive(:current_user) {:user}
patch :update, { user: { :billing_address => "My Second address" } }
user.reload
end
A controller spec should test the behavior of the action. Your action's behavior can be roughly described as:
Get the permitted parameters
Update the user
If the update succeeded, redirect
Otherwise redisplay the edit page
Updating the user is the responsibility of the model, not the controller. If you are concerned that a particular set of parameters will (or won't) update the user instance, create a model spec and test the parameters there.
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
I have an Admin model which can manage Organizations.
I have an AdminController with a simple index action and a child Admin::OrganizationsController controller which I'm trying to test.
The test for the canonical show action on this child controller passes without errors:
describe "GET show" do
it "assigns the requested organization as #organization" do
org = FactoryGirl.create(:organization)
get :show, id: org.id # <---- this works
expect(assigns(:organization)).to eq(org)
end
end
but when I try to test the destroy action, I get an error I'm not able to understand (hence resolve):
describe "DELETE destroy" do
it "destroys the requested organization" do
org = FactoryGirl.create(:organization)
delete :destroy, id: org.id # <---- (I tried to use org.id.to_param, unsuccessfully)
# ...rest of the test
end
end
with error:
Failure/Error: expect { delete :destroy, id: org.id }.to change(Organization, :count).by(-1)
NameError:
undefined local variable or method `organizations_url' for #<Admin::OrganizationsController:0x007fefe1622248>
I suspect this has to do with my controller being "nested" (it needs something like admin_organizations_url I guess).
Any help?
(additional side infos: Rails 4.0.1, rspec 3.0.0.beta1)
"Inspired" by CDub's comment, I took a look at the destroy action in Admin::OrganizationController, which looked like this:
def destroy
#organization.destroy
respond_to do |format|
format.html { redirect_to organizations_url } # <--- has to be admin_organizaions_url
format.json { head :no_content }
end
end
I didn't pay attention to the respond_to block at all.
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'} }
I believe I am having problems with the redirect_to in my controller below. A course has many levels, which has many steps (step belongs to level, and level belongs to course). I am trying to redirect_to the step.
Here is my route for a step:
/courses/:course_id/levels/:level_id/steps/:id(.:format)
Here is my error in the controller:
Failure/Error: click_button "check answer"
NoMethodError:
undefined method `step_url' for #<UserStepsController:0x007ff8ccc478c8>
Here is the controller in which the error occurs:
class UserStepsController < ApplicationController
before_filter :authenticate_user!
def create
#step = Step.find(params[:user_step][:step_id])
current_user.attempt_step!(#step)
respond_to do |format|
format.html { redirect_to #step }
format.js
end
end
def destroy
#step = UserStep.find(params[:id]).step
current_user.remove_user_step!(#step)
respond_to do |format|
format.html { redirect_to #step }
format.js
end
end
end
Here is the rspec
describe "attempting a step" do
let(:course) { FactoryGirl.create(:course)}
let(:level) { FactoryGirl.create(:level, course: course) }
let(:step) { FactoryGirl.create(:step, level: level) }
before { sign_in user}
describe "taking a course" do
before { visit course_level_step_path(course.id, level.id, step.id)}
it "should increment the user step count" do
expect do
click_button "check answer"
end.to change(user.user_steps, :count).by(1)
end
describe "toggling the button" do
before { click_button "check answer"}
it { should have_selector('input', value: 'remove step')}
end
end
You can't use redirect_to #variable when you have nested resources. The correct way would be for you to say
redirect_to course_level_step_url(#step.level.course.id, #step.level.id, #step.id)
Because the thing that gets the route from the instance variable 'thinks' you want the path of the lowercased class name plus _url
I'm writing an rspec scenario thats failing with:
(#<User:0x1056904f0>).update_attributes(#<RSpec::Mocks::ArgumentMatchers::AnyArgMatcher:0x105623648>)
expected: 1 time
received: 0 times
users_controller_spec.rb:
describe "Authenticated examples" do
before(:each) do
activate_authlogic
#user = Factory.create(:valid_user)
UserSession.create(#user)
end
describe "PUT update" do
it "updates the requested user" do
User.stub!(:current_user).and_return(#user)
#user.should_receive(:update_attributes).with(anything()).and_return(true)
put :update, :id => #user , :current_user => {'email' => 'Trippy'}
puts "Spec Object Id : " + "#{#user.object_id}"
end
users_controller.rb:
def update
#user = current_user
puts "Controller Object ID is : " + "#{#user.object_id}"
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to(root_url, :notice => 'Successfully updated profile.') }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #user.errors, :status => :unprocessable_entity }
end
end
end
user.rb - factories
Factory.define :valid_user, :class => User do |u|
u.username "Trippy"
u.password "password"
u.password_confirmation "password"
u.email "elephant#gmail.com"
u.single_access_token "k3cFzLIQnZ4MHRmJvJzg"
u.id "37"
end
Authlogic's standard helper methods like current_user don't call User.find directly. I believe it does current_user_session.user, where current_user_session calls UserSession.find, so you're not calling User.find directly. You could do some fancy chain stubbing there, but my suggestion is just to add this to your controller spec instead of what you're currently stubbing:
stub!(:current_user).and_return(#user)
In RSpec2 you might have to do
controller.stub!(:current_user).and_return(#user)
Edit: This should be your whole spec file:
describe "Authenticated examples" do
before(:each) do
activate_authlogic
#user = Factory.create(:valid_user)
UserSession.create(#user)
end
describe "PUT update" do
describe "with valid params" do
it "updates the requested user" do
stub!(:current_user).and_return(#user)
#user.should_receive(:update_attributes).with(anything()).and_return(true)
put :update, :id => #user , :current_user => {'email' => 'Trippy'}
end
end
I think you're confusing stubs with message expectations. The line
User.should_receive(:find)
tells Rspec to expect the User model to receive a find message. Whereas:
User.stub!(:find)
replaces the find method so that the test can pass. In your example the thing you're testing is whether update_attributes is called successfully, so that ought to be where the message expectation goes, and the job of all the other testing code is just to set up the prerequisites.
Try replacing that line with:
User.stub!(:find).and_return(#user)
Note that find returns the object, not just its id. Also, note that stubbing out find here serves only to speed things up. As written the example gets through should_receive(:find) successfully, and that is happening because you're using Factories to create users in the test database. You could take the stub out and the test should still work, but at the cost of hitting the database.
Another tip: if you're trying to figure out why a controller test isn't working, sometimes it's helpful to know if it is being blocked by before filters. You can check for this with:
controller.should_receive(:update)
If that fails, the update action is not being reached, probably because a before filter has redirected the request.