After we implemented logic around User attributes, it is time to finally save and update
the User to the database.
But first, we shall change a mistake from the post on creating Sign Up page, the password
fields in the View are set as normal text. We shall make sure that it is a password field!
Change the text_field to password_field in app/views/users/_form.html.haml:
Now when the password fields are fixed, lets create a test to save a Valid User to the
database.
Create a new context for POST in spec/requests/users_spec.rb:
1234567891011121314
context'POST /users'doit'creates and saves the valid user'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content'The User is successfully saved!'endend
The test is pretty simple, we fill up the form with data, click ‘Sign Up’ button, and
expect to be redirected again to “Sign Up” page (in new post when we create a Login page
the create action will be redirecting to it!) and we expect that the flash notice will show that
User is successfully saved.
The test should fail on the clicking the Sign Up button, because there is no create
action in UsersController:
1234
1)UsersPOST/users creates and saves the valid user Failure/Error:click_button'Sign Up'AbstractController::ActionNotFound:Theaction'create'couldnotbefoundforUsersController
So to pass this test we need to add a create method to the UsersController:
12345678
defcreate@user=User.newparams[:user]if@user.saveflash[:notice]='The User is successfully saved!'redirect_tosignup_pathendend
First we storing a User to instance variable @user from the data given in User Sign Up
Form as params[:user]. Then we try to save it, and on success, we assign the flash notice
and redirect back to Sign Up Page.
The test should pass:
Finished in 1.37 seconds
11 examples, 0 failures
The happy path is working, now lets test all the possible ways the Sign Up might go wrong!
Invalid User Tests
Password Confirmation
Lets first check passwords matching is working, by applying the two different passwords.
We shall crate the new context inside POST /userscontext and call it ”not saving invalid user”:
After running test we shall get odd message that there is no View Template for create.
12345
1)UsersPOST/users not saving invalid user when passwords mismatch Failure/Error:click_button'Sign Up'ActionView::MissingTemplate:Missingtemplateusers/create,application/createwith{:locale=>[:en],:formats=>[:html],:handlers=>[:erb,:builder,:coffee,:haml]}.Searchedin:*"/Users/xajler/src/rb/just-todo-it/app/views"
We don’t need a new View Template but rather change the UserController method create
to do something if the user is not saved. And when is not saved we shall flash an error message:
1234567891011
defcreate@user=User.newparams[:user]if@user.saveflash[:notice]='The User is successfully saved!'redirect_tosignup_pathelseflash[:error]=@user.errors.full_messages[0]redirect_tosignup_pathendend
We are setting as a flash message from a User instance errors and getting first full_message
that will be set upon having problem saving user and in this case message will be
”Password doesn’t match confirmation”.
After applying the given code, the test should pass:
Finished in 1.54 seconds
12 examples, 0 failures
With this code applied we also satisfy the tests that are yet to come, but we still need
tests to prove that create method is working and proper error messages are thrown to
guide the users of Application to resolve them!
Required Fields
We have three required fields:
email
password
full_name
Lets write three more test, they should all be green since the implementation is written
to satisfy the previous test.
We will add them to the ”not saving invalid user” context below the latest test:
it'when email is blank'dovisitnew_user_pathfill_in'Email',with:''fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Email can't be blank"endit'when password is blank'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:''fill_in'Confirm Password',with:''fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Password digest can't be blank"endit'when full name is blank'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:''click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Full name can't be blank"end
So, in first test we test required Email, by sending blank one. In second the Passwords
are blank and in third the Full Name is blank. And each test expect that meaningful messages
are shown to the user of application!
And if all was OK then all tests should pass:
Finished in 1.79 seconds
15 examples, 0 failures
Email Uniqueness
The saving User with entered existing email should fail. Lets create the test to prove
it:
12345678910111213
it'when email is not unique'docreate:uservisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content'Email has already been taken'end
So, first we insert User from our created factory via Factory Girl. The factory of User
inserted has the same email that we try to save it from the User Sign Up Form. And again
we test to get meaningful message when email is not unique.
The test should pass, because the code is implemented:
Finished in 2.02 seconds
16 examples, 0 failures
Password with at least 8 characters
The User cannot be saved if both passwords length is at least 8 characters long:
123456789101112
it'when password is less than 8 characters'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'123'fill_in'Confirm Password',with:'123'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Password is too short (minimum is 8 characters)"end
The test applies the Passwords that are of 3 characters long and we expect to get right message
that is telling the right expectations of Password in this case minimum of 8 characters.
This was easy, all tests should pass still:
Finished in 2.07 seconds
17 examples, 0 failures
Note:
If you using Guard as I do, for running test, you may need to enter r command to
Guard console to reload all, only if your tests are passing and they should not be or vice versa!
And with this test we have test for all User Create problems, now we’ll add test for User Update.
Valid Update Test
Essentially, the valid update only lets to change User password and full_name. Create
new context called ”PUT users/:id” and new it block ”valid user update”:
123456789101112131415161718
context'PUT users/:id'doit'valid user update'douser=create:uservisitedit_user_pathuserfind_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler'fill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeuidht'fill_in'Full Name',with:'Kornelije Sajler - xajler'click_button'Update User'current_path.should==edit_user_path(user)page.shouldhave_content'The User is successfully updated!'endend
First we create a User from factory and save it as variable, because we are simulating edit,
so we need the User id when we want to edit him. This is why we pass the user variable to the
edit_user_path.
Before we fill in the User Update Form we shall test it if the Form fields are actually binded with
data from User that will be updated.
OK, same thing as for new/create it will fail, first, because there is no method edit in
UsersController.
1234
1)UsersPOST/users PUT users/:idvaliduserupdateFailure/Error:visitedit_user_path#userActionController::RoutingError:Noroutematches{:action=>"edit",:controller=>"users"}
To pass this error lets create the edit method:
123
defedit@user=User.findparams[:id]end
So we need the User to get him from the given params id so that we can have it in
instance variable @user and share it to the View and then fill the form with existing
User data.
The test should still fail, because there is no edit View Template:
12345
1)UsersPOST/users PUT users/:idvaliduserupdateFailure/Error:visitedit_user_pathuserActionView::MissingTemplate:Missingtemplateusers/edit,application/editwith{:locale=>[:en],:formats=>[:html],:handlers=>[:erb,:builder,:coffee,:haml]}.Searchedin:*"/Users/xajler/src/rb/just-todo-it/app/views"
Long time ago when we are creating View Template for Sign Up we’ve created new.html.haml
and also separated form to partial _form.html.haml so it can be shared with Edit User View.
Lets create edit.html.haml View page that calls the Form partial and passing to it the
@user so that the Form will display User data:
123
%h1 Update User
=renderpartial:'form',locals:{user:@user}
The test will fail because there is no button “Update User”, and this is because we hard-coded
the “Sign Up” text to submit element in the form partial _form.html.haml. We will fix it now,
not very pretty but it will work:
Change submit in _form.html.haml from:
1
=f.submit'Sign Up'
To:
1
=f.submit@user.id?'Update User':'Sign Up'
The test should still fail because we are after all trying to update, and there in no
update method it UsersController:
1234
1) Users POST /users PUT users/:id valid user update
Failure/Error: click_button 'Update User'
AbstractController::ActionNotFound:
The action 'update' could not be found for UsersController
So, we shall impelment the update method for the UsersController:
12345678
defupdate@user=User.findparams[:id]if@user.update_attributesparams[:user]flash[:notice]='The User is successfully updated!'redirect_toedit_user_pathendend
First we getting User from the database from given User Id, and then call update_atrributes
from the data send via Form as params[:user]. Set the flash notice that User is updated
and redirect to Update User page where we can see that User is actually updated.
And at last the test is passing:
Finished in 2.3 seconds
18 examples, 0 failures
The only problem here is that to Update User the passwords needed to be entered every
time. There are better solutions, like if they are blank, to ignore them on update, instead
currently it will be validated. And blank password is not allowed!
Password Mismatch
We will not try all the tests that we tried for User Sign Up, but only one. If you remember,
this was the only test that was needed a code, others were just a proof of existing code,
that is more about User Model than actual User View, but we’ve needed to run them all to
ensure that flash messages are proper and expected.
12345678910111213
it'invalid when passwords mismatch'douser=create:uservisitedit_user_pathuserfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeu'fill_in'Full Name',with:'Kornelije Sajler'click_button'Update User'current_path.should==edit_user_path(user)page.shouldhave_content"Password doesn't match confirmation"end
And the test fails, complaining about View, but actually we need the code in Controller,
to send message of what went wrong to User Update Form:
1234567891011
defupdate@user=User.findparams[:id]if@user.update_attributesparams[:user]flash[:notice]='The User is successfully updated!'redirect_toedit_user_pathelseflash[:error]=@user.errors.full_messages[0]redirect_toedit_user_pathendend
After adding the else clause to update method that handles all errors, the test
should pass:
Finished in 2.6 seconds
19 examples, 0 failures
Ensure Email is not changed after User creation
The only test that is left is to ensure that email given on create should never ever
be possible to change!
In this test we shall try to change the email of existing User and it needs to be the same
after update, even though the email sent to update is quite different from the one when
User is created:
1234567891011121314151617
it'keeps the User Email intact while other fields do change'douser=create:uservisitedit_user_pathuserfind_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler'fill_in'Email',with:'xxx@example.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeuidht'fill_in'Full Name',with:'Kornelije Sajler - xajler'click_button'Update User'current_path.should==edit_user_path(user)find_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler - xajler'end
The above test ensures that the email after creation will stay intact, while other fields
will change, there will be no errors having different email. The email for update will
be silently ignored.
The implementation code of this validation is actually in User Model,
making the email as attr_readonly that we did in previous post, this test only ensures
that validation is implemented from View perspective.
And now all 20 test should pass, and we use RSpec format to show them all:
User
#email must not ever change after it is created
is valid
is invalid
when required #email is not given
when #email format is not valid
when #email is not unique
when required #full_name is not given
when required #password is not given
when #password is not at least 8 characters
Users
GET /signup
displays the sign up page
PUT users/:id
valid user update
invalid when passwords mismatch
keeps the User Email intact while other fields do change
POST /users
creates and saves the valid user
not saving invalid user
when passwords mismatch
when email is not unique
when full name is blank
when password is blank
when email is blank
when password is less than 8 characters
GET /users/new
displays the create new user page
Finished in 2.86 seconds
20 examples, 0 failures
Users Controller
The whole Users Controller app/controllers/users_controllers.rb after this post should look like this:
classUsersController<ApplicationControllerdefnew@user=User.newenddefcreate@user=User.newparams[:user]if@user.saveflash[:notice]='The User is successfully saved!'redirect_tosignup_pathelseflash[:error]=@user.errors.full_messages[0]redirect_tosignup_pathendenddefedit@user=User.findparams[:id]enddefupdate@user=User.findparams[:id]if@user.update_attributesparams[:user]flash[:notice]='The User is successfully updated!'redirect_toedit_user_pathelseflash[:error]=@user.errors.full_messages[0]redirect_toedit_user_pathendendend
Users Request Spec (Tests)
The whole Users Request Spec spec/requests/users_spec.rb after this post should look like this:
require'spec_helper'describe'Users'docontext'GET /users/new'doit'displays the create new user page'dovisitnew_user_pathpage.shouldhave_content'Email'page.shouldhave_content'Full Name'page.shouldhave_content'Password'page.shouldhave_content'Confirm Password'page.has_field?'email'page.has_field?'full_name'page.has_field?'password'page.has_field?'password_confirmation'page.has_button?'Sign Up'endendcontext'GET /signup'doit'displays the sign up page'dovisitsignup_pathpage.shouldhave_content'Email'page.shouldhave_content'Full Name'page.shouldhave_content'Password'page.shouldhave_content'Confirm Password'page.has_field?'email'page.has_field?'full_name'page.has_field?'password'page.has_field?'password_confirmation'page.has_button?'Sign Up'endendcontext'POST /users'doit'creates and saves the valid user'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content'The User is successfully saved!'endcontext'not saving invalid user'doit'when passwords mismatch'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x123'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Password doesn't match confirmation"endit'when email is blank'dovisitnew_user_pathfill_in'Email',with:''fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Email can't be blank"endit'when password is blank'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:''fill_in'Confirm Password',with:''fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Password digest can't be blank"endit'when full name is blank'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:''click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Full name can't be blank"endit'when email is not unique'docreate:uservisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'x1234567'fill_in'Confirm Password',with:'x1234567'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content'Email has already been taken'endit'when password is less than 8 characters'dovisitnew_user_pathfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'123'fill_in'Confirm Password',with:'123'fill_in'Full Name',with:'Kornelije Sajler'click_button'Sign Up'current_path.should==signup_pathpage.shouldhave_content"Password is too short (minimum is 8 characters)"endendendcontext'PUT users/:id'doit'valid user update'douser=create:uservisitedit_user_pathuserfind_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler'fill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeuidht'fill_in'Full Name',with:'Kornelije Sajler - xajler'click_button'Update User'current_path.should==edit_user_path(user)page.shouldhave_content'The User is successfully updated!'endit'invalid when passwords mismatch'douser=create:uservisitedit_user_pathuserfill_in'Email',with:'xajler@gmail.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeu'fill_in'Full Name',with:'Kornelije Sajler'click_button'Update User'current_path.should==edit_user_path(user)page.shouldhave_content"Password doesn't match confirmation"endit'keeps the User Email intact while other fields do change'douser=create:uservisitedit_user_pathuserfind_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler'fill_in'Email',with:'xxx@example.com'fill_in'Password',with:'aoeuidht'fill_in'Confirm Password',with:'aoeuidht'fill_in'Full Name',with:'Kornelije Sajler - xajler'click_button'Update User'current_path.should==edit_user_path(user)find_field('Email').value.should=='xajler@gmail.com'find_field('Full Name').value.should=='Kornelije Sajler - xajler'endendend
Conclusion
In this post we made sure that User Model Validation that is tested through actual User
Sign Up and Update Views. And all aspects of Validation are tested in integration Broweser
Tests simulated with Capybara.
In next post we shall finally tackle the Login page and implementing Authentication for
Application.
Code
The code is hosted on GitHub and can be cloned from the xajler/just-todo-it.