1

FEATURE

A user has a profile and should be able to update it.


PROBLEM

I update the profile, for example change the name to "Homer Simpson", but all the assertions fail as the database record does not seem to update.

I can't seem to get updated attributes:

 Failure/Error: expect(subject.current_user.first_name).to eq('Homer')

   expected: "Homer"
        got: "Lew"

   (compared using ==)
 # ./spec/controllers/registrations_controller_spec.rb:67:in `block (3 levels) in <top (required)>'

N.B. I have tried both @user.reload and subject.current_user.reload

Specs still not passing.


CODE

I am using:

  • rails (4.0.0)
  • devise (3.0.3)
  • rspec-rails (2.14.0)
  • capybara (2.1.0)
  • factory_girl (4.2.0)
  • database_cleaner (1.1.1)

I have already checked:

registrations_controller_spec.rb

describe "User Profiles" do
  login_user

  it "Update - changes the user's attributes" do
    put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
    @user.reload
    expect(@user.first_name).to eq('Homer') # FAILS
  end
end

I have tried swapping @user for subject.current_user like in this Stackoverflow thread: "Devise Rspec registration controller test failing on update as if it was trying to confirm email address"

  put :update, id: subject.current_user, user: attributes_for(:user, first_name: 'Homer')
  subject.current_user.reload
  expect(subject.current_user.first_name).to eq('Homer') # Still FAILS

But it still fails.

Is the problem in the controller? I find the user by current_user.id instead of via params[:id].

registrations_controller.rb

def update
  @user = User.find(current_user.id)
  email_changed = @user.email != params[:user][:email]
  password_changed = !params[:user][:password].blank?

  if email_changed or password_changed
    successfully_updated = @user.update_with_password(user_params)
  else
    successfully_updated = @user.update_without_password(user_params)
  end

  if successfully_updated
    sign_in @user, bypass: true # Sign in the user bypassing validation in case his password changed
    redirect_to user_profile_path, notice: 'Profile was successfully updated.'
  else
    render "edit"
  end
end

controller_macros.rb - defines login_user helper

module ControllerMacros
  def login_user    
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:user]
      @user = FactoryGirl.create(:user)
      @user.confirm!
      sign_in @user
    end
  end
end

My integration specs pass fine. What am I missing here in the controllers?

Community
  • 1
  • 1
julie-ng
  • 500
  • 6
  • 11

3 Answers3

2

My answer can solve your problem but not a direct fix of the bugs in your code. To do that I need to write more tests and hands-on debugging, I'm not so experienced to figuring it out by reading only :)

I do not suggest you to override Devise's RegistrationsController like that in question. Comparing with original code, your code lacks of two things at least:

  1. No copy of current_user object. In real app the current_user will be logged out by submitting the form which is not nice.

  2. Lack of sanitize of parameters

And the remaining bugs.

My suggestion is to use Devise's method directly because there is nothing special in your code and no need to override full code.

class RegistrationsController < Devise::RegistrationsController
  def update
  end
  # Or even without this method.
end

That's all.

For no requiring of password

def update
  params.merge!(password: current_user.password) if params[:password].blank?
  super
end

For tests, just write some casual integration tests. Devise has full covered functional tests so no need to repeat.

Billy Chan
  • 24,625
  • 4
  • 52
  • 68
  • Sorry, I disagree. Using default update is not an option. I wanted to [allow users to edit accounts without providing passwords - as per the devise wiki](https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-edit-their-account-without-providing-a-password). – julie-ng Sep 25 '13 at 17:49
  • As I said in the original post, my integration specs past. I'm really asking the question to try to understand the problem i.e. rspec and devise better. So far I'm none the wiser. (sorry, I hit enter too soon on the comment above) – julie-ng Sep 25 '13 at 17:50
  • jng5, I don't understand why your integration tests passed because apparently the user object failed at updating in this controller spec. For the Wiki part, I can't comment because I have not used that snippet but the snippet looks out of date as it is far different from what the current code is. – Billy Chan Sep 25 '13 at 18:04
  • jng5, a version of not requiring password overriding :) – Billy Chan Sep 25 '13 at 18:09
  • Well the functionality works as expected (when tested by humans, not just integration) so I think it's a controller test problem. As I posted, my best guest would be that in the controller, I pull the user from `current_user` instead of `params`. Again, none the wiser... – julie-ng Sep 25 '13 at 20:40
0

try assigns

it "Update - changes the user's attributes" do
  put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
  homer = assigns(:user)
  @user.reload
  expect(homer.first_name).to eq('Homer')
end

update: based on Billy Chan's comment, this should correctly test that the name is being updated

it "Update - changes the user's attributes" do
  put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')
  homer = assigns(:user)
  @user.reload
  #expect(homer.first_name).to eq('Homer') Peter and Billy are right, this only tests
  # that the attribute was actually assigned, not that the update was successful
  expect(@user.first_name).to eq(homer.first_name)
  #however this test that the users updated `first_name` matches the attribute 
  #in the test 
end

Note:

I'm basing this answer on the Michael Hartl tutorial which I went through a few months ago - he uses this method, and I believe he explains why - although I don't have the screen casts in front of me at the moment. I'll look it up later.

Video:

Here's the video - it's very low quality because i just used quicktime's screen record - and there's some a brutal feedback loop in the beginning so mute your computer for the first few seconds.

dax
  • 10,779
  • 8
  • 51
  • 86
  • It works! Thank you! Why do I need `assigns`, esp. if I then do `@user.upload`? Just trying to understand. Thanks again. – julie-ng Sep 25 '13 at 14:46
  • glad it's working! check out the [documentation](http://rubydoc.info/gems/rspec-rails/frames) on assigns - it "Assigns a value to an instance variable in the scope of the view being rendered." - if this helped, please tick my answer. thanks! – dax Sep 25 '13 at 14:48
  • Yes, I looked up the function. But I still don't understand why I need to do the roundabout way with `assigns` instead of accessing `@user` or `subject.current_user` directly. It wouldn't let me immediately tick your answer as correct ;-) – julie-ng Sep 25 '13 at 14:52
  • 1
    Doesn't this test just now check the value of `@user.first_name` in the controller? Does that necessarily mean that the `put` worked as expected? I would think the test should work as originally specified if the `put` succeeded. – Peter Alfvin Sep 25 '13 at 14:59
  • @PeterAlfvin - yes, I think so - the user attributes are being assigned to `homer` but they're not loaded - reload loads the test database and then `homer` is accessible – dax Sep 25 '13 at 14:59
  • For anyone reading this, @dax's comment above was in response to a comment I since deleted asking if `@user.reload` was still needed. – Peter Alfvin Sep 25 '13 at 15:01
  • @dax, but OP does have `reload` in his question. – Billy Chan Sep 25 '13 at 15:02
  • BTW, is `put` smart enough to accept `@user` instead of `@user.id` for the `id:` parameter? – Peter Alfvin Sep 25 '13 at 15:03
  • @dax I saw your update, but Michael has no references to `assigns` in his tutorial (current or http://ruby.railstutorial.org/book/ruby-on-rails-tutorial?version=3.2) and he uses `.reload` to double check that the database has been updated. I don't understand in what sense you mean that this is his technique. I'd really appreciate if you would look it up later as you indicated you would. – Peter Alfvin Sep 25 '13 at 15:11
  • 1
    The test will send false signal according to your code. If update failed, `edit` template will be rendered, and the assigned user's first name will be `homer`. The test passed, but it's not expected result. The `update` is failed. – Billy Chan Sep 25 '13 at 15:12
  • @dax @BillyChan the updated answer with `expect(@user.first_name).to eq(homer.first_name)` spec fails. My integration/feature spec still passes though… – julie-ng Sep 25 '13 at 15:36
  • @jng5, try `@user.first_name.should == homer.first_name` – dax Sep 25 '13 at 15:39
  • Guys, I don't see how the `homer` variable is adding any value here. Aren't we back to the OP's original problem? – Peter Alfvin Sep 25 '13 at 15:41
  • @BillyChan Can you explain to me how the controller instance variable `@user` picking up the `Homer` first name in the case of the `edit` render? – Peter Alfvin Sep 25 '13 at 15:44
  • added video - sorry for the very low quality! – dax Sep 25 '13 at 15:47
  • @PeterAlfvin, try this in your console in an app with Devise: `user = create :user; user.update_with_password(email: 'foo@bar.com', name: 'foo')`. It will return false with attributes changed. – Billy Chan Sep 25 '13 at 15:55
  • 2
    @dax Thanks so much for taking the time to make and post the video. I see what you mean about this practice. To the extent that controller tests are in part about establishing instance variables for views to use, I can see how testing them makes sense. I still have some questions, but I want to think about them. Specifically, it seems there are three values involved in an update: 1) the new values being set, 2) the values in the database after the operation is complete, and 3) the values in the instance variable. Just checking that any two of them are equal seems problematic. – Peter Alfvin Sep 25 '13 at 17:05
  • @dax - it turns out I didn't need assigns after all… but thanks very much for helping me wrap my head around all this – julie-ng Sep 26 '13 at 09:24
0

Answer

Thanks everyone for their suggestions, which helped me wrap my head around my code and find the problem.

CAUSE OF FAIL: Default factory included params included email and password, so controller test kept trying to change the user's password.

Specifically I changed this line of code in registrations_controller_spec.rb

put :update, id: @user, user: attributes_for(:user, first_name: 'Homer')

to:

patch :update, id: @user, user: attributes_for(:user_params, first_name: 'Homer', last_name: 'Simpson')

Then I had to update my factory, so I can use :user_params instead for updates:

FactoryGirl.define do

  factory :user do
    first_name          { Faker::Name.first_name }
    last_name           { Faker::Name.last_name }
    sequence(:username) { |n| "user-#{n}" }
    email               { Faker::Internet.email }
    password            { Faker::Lorem.characters(8) }
  end

  factory :user_params, class: :user do
    first_name     { Faker::Name.first_name }
    last_name      { Faker::Name.last_name }

    factory :user_params_with_email, class: :user do
      email        { Faker::Internet.email }
    end

    factory :user_params_with_password, class: :user do
      password    { Faker::Lorem.characters(8) }
    end
  end

end

Thanks for everyone who made suggestions. It helped me unravel my code and @billy-chan was right in pointing out issues, which I fixed.

  • Params weren't sanitized (am in process of finishing rails4 upgrade)
  • Other misc. bugs

Lesson learned

Compare params going in and out of controllers.

My integration tests were passing because I wasn't trying to change the email or password.

julie-ng
  • 500
  • 6
  • 11