Mar 23, 2016
Table of contents:
Over the last couple of weeks we’ve looked at a number of different aspects of creating a web application using Ruby on Rails.
From creating Models and Form Objects, to working with Mailers, Queues, and writing Functional Tests, we’ve already covered quite a lot of ground up until now.
However, the real value in learning about these individual components of the framework is when you put them together to meet the requirements of the application you want to build.
One of the first things that a user is likely going to interact with your application is the registration process.
The typical registration process is usually fairly simple, but has requirements that are slightly more involved than the typical example.
So in today’s tutorial we’re going to look at what it takes to build a registration process for a Ruby on Rails application from scratch.
Before we jump into the code, first I will briefly outline how this is going to work.
I’m going to be building the registration flow for Culttt. This will involve creating a new user, assigning the user’s role, and emailing a confirmation.
I’m going to need to create the Migration, the Model, the Form Object, the Controller, the View, and I’m going to need to write Unit and Functional tests where appropriate.
Hopefully this will be more of a realistic example than your typical tutorial, but without getting into the weeds of really specific requirements of a complicated enterprise application.
If you are looking to build that type of application, hopefully this will give you a good foundation that you can extend to satisfy your requirements.
This is going to cover a lot of ground and so I’ve split it over the next couple of weeks. In today’s tutorial we’re going to be looking at creating the Models and writing Unit Tests.
All of the code for this mini series is available on Github.
We’re going to start this mini series totally from scratch and so the first thing we need to do is to create a new Rails project. I’m going to assume you already have Rails installed on your computer. If not, take a look at Getting started with Ruby on Rails.
Once you have Rails installed, run the following command in Terminal:
rails new rails_registration_tutorial
This will create a new Ruby on Rails project for you in your current directory.
The next thing we’re going to do is to create the User
Model. Typically the User
Model is going to be one of the most important Models in a web application, but this is especially the case for this tutorial as we’re focusing on the registration flow.
If you remember back to Creating your first Ruby on Rails Model, we can use the Rails Generators to create the Model, the Migration and the Test files we will need.
To create the User Model, run the following command in Terminal:
bin/rails g model User email:string password_digest:string confirmed_at:timestamp
This should output a list of the generated files that Rails automatically created for us.
First us we have the migration file under the db/migrate
directory:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string, :email
t.string, :password_digest
t.timestamp :confirmed_at
t.timestamps null: false
end
end
end
I’m going to make some adjustments to this file to add some database level restrictions:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string, :email, unique: true, null: false
t.string, :password_digest, null: false
t.timestamp :confirmed_at
t.timestamps null: false
end
end
end
I’ve modified this migration so that the email
column is a unique index and both the email
and password_digest
columns cannot be null
.
With those modifications in place, we can now run migrate the database to set up the users
table. Run the following command in Terminal:
bin/rake db:migrate
Now that we’ve got the User
model set up we can write a couple of tests to ensure that it is following the business rules of the application correctly.
However, before we do that, first I’m going to add a couple of Gems to the application that will make testing easier.
Add the following to your Gemfile
:
group :test do
gem 'shoulda'
gem 'faker'
gem 'factory_girl_rails'
end
And then run the following command in Terminal:
bundle install
I’ve previously talked about using these Gems in the following tutorials so I won’t repeat myself here:
You can also delete the users.yml
file from the test/fixtures
directory as we’re not going to be use Fixtures in this tutorial.
The business rules that involve the User
model are really quite simple at this stage because we don’t have a lot going on.
As your application evolves, the complexity of the business rules that concern the User
model are likely to get a whole lot more complicated.
But for now, this should be an easy introduction to the process.
Firstly, open up the user_test.rb
file under the test/models
directory. It should currently look like this:
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
Before I write an actual tests, I’m going to add a setup
method that will create a new instance of the User
model before each test:
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@subject = User.new
end
end
The first test I will write will be to assert that the email
address property is required:
test 'email should be required' do
@subject.valid?
assert_includes(@subject.errors[:email], "can't be blank")
end
To run this test, run the following command in Terminal:
bin/rake test test/models/user_test.rb
You should see the test fail. With a failing test in place we can write the code to make it pass.
If you open the user.rb
file under the app/models
directory you should see the following code:
class User < ActiveRecord::Base
end
To make the test pass, we can add a validation rule to the model that will ensure that the email
property has been set:
class User < ActiveRecord::Base
validates :email, presence: true
end
If you run the test from earlier again, you should see it pass. Congratulations, you’ve just written your first Unit Test and the code to make it pass!
Tests to check that a property of a model are so common we can use one of the Shoulda helpers to make this test more concise. You can replace the test you just wrote with this one line:
class UserTest < ActiveSupport::TestCase
def setup
@subject = User.new
end
should validate_presence_of(:email)
end
If you run the tests again, you will see that it still passes!
Next we’re going to write a test to assert that the user’s email is valid. You can’t trust incoming data to be valid and so this is an important test to assert the integrity of the data.
test 'email should be a valid email' do
@subject.email = 'invalid'
@subject.valid?
assert_includes(@subject.errors[:email], 'is not an email')
end
In this test I’m setting the email
property to be "invalid"
which is obviously not a real email address. I’m then asserting that the validation rule is complaining about an invalid email address.
If you run the tests again you should see the test we just wrote fail. So with a failing test in place, we can write the code to make it pass.
To check to make sure the email address that the user has provided is a valid email address we need to assert that it is of the correct format.
To do that, we can create our own email validator. Create a new directory under app
called validators
, and then create a new file called email_validator.rb
:
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || 'is not an email')
end
end
end
This validator will assert that the value is the correct format using a Regex. If the value does not match against the Regex, an error will be added to the errors
array.
To add this validation rule to the User
model we can simply update it to look like this:
class User < ActiveRecord::Base
validates :email, presence: true, email: true
end
Now if you run the tests again you should see them pass!
Next I will add a test to assert that the given email address is unique. If you remember back to the migration we added a database constraint to ensure that email addresses are unique. So instead of allowing the database to throw an Exception
we can add this as a business rule to the model.
First we will write the test:
test 'email should be unique' do
create(:user, email: 'email@domain.com')
@subject.email = 'email@domain.com'
@subject.valid?
assert_includes(@subject.errors[:email], 'has already been taken')
end
In this test I’m first creating a new user with a set email address using the create
method, setting the factory using a Symbol (What are Symbols in Ruby?) and then manually setting the email address by passing a hash as the second argument.
Next I’m setting the new instance of the User
model to have the same email address.
And then finally I’m asserting that the correct error message has been added to the errors
array.
Before running this test I’m going to add a new directory under test
called factories
. This is where we need to keep the Factory Girl factories. If this doesn’t make sense to you, take a look at Replacing Fixtures with Factory Girl in Ruby on Rails.
Next, create a new file called user.rb
under the factories
directory:
FactoryGirl.define do
factory :user do
email Faker::Internet.safe_email
password Faker::Internet.password
end
end
And finally update your test_helper.rb
file to look like this:
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
include FactoryGirl::Syntax::Methods
end
Now if you run the tests again, you should get a strange error:
# NoMethodError: undefined method `password=' for #<User:0x007f893abeeb80>
Now that we’ve started to use Factory Girl, we’re getting a weird error saying there is no password=
method!
The reason why we’re getting this error is because Factory Girl is trying to set a password
property, but the property on the User
model is actually password_digest
.
Rails authentication will save the user’s password digest in the password_digest
database column, but will provide the password=
method for us. The reason why we’re getting this error is because we haven’t added the authentication functionality to the User
model yet.
To add the authentication functionality, update your user.rb
file to look like this:
class User < ActiveRecord::Base
has_secure_password
validates :email, presence: true, email: true
end
You will also need to uncomment the bcrypt
Gem from your Gemfile
:
gem 'bcrypt', '~> 3.1.7'
Next install the bcrypt
Gem by running the following command in Terminal:
bundle install
Finally if you run the test again you should see the correct error:
Expected [] to include "has already been taken"
To make this test pass we can add a uniqueness validation rule to the User
model:
class User < ActiveRecord::Base
has_secure_password
validates :email, presence: true, email: true, uniqueness: true
end
If you run the tests again, you should see them all pass.
Due to the fact that we’ve mixed the concerns a bit in this test by prematurely adding the authentication functionality to the model, we can also add the tests for this functionality now too:
test 'user should be authentic able' do
user = create(:user, password: 'password')
assert_not(user.authenticate('qwerty'))
assert(user.authenticate('password'))
end
Here I’m using Factory Girl to create a new User
object with a known password.
Next I’m asserting that the authenticate
method returns false
for an incorrect password and true
for a correct password.
We can also add another presence
assertion for the password
property:
should validate_presence_of(:password)
If you run the tests again you should see them all pass because we’ve already added this functionality to the User
model.
If you remember back to when we generated the user migration I included a timestamp
column called confirmed_at
.
This is to restrict access to the user until they have clicked on a confirmation link in an email that I will send them when they sign up to the application.
By default the user will not be confirmed when they first register. I will need to add a method to the User
model to confirm the user once they have clicked the link, and a method to check to see if the user is confirmed for when I’m restricting access to certain functionality.
We can add the following test to assert this business logic:
test 'should confirm the user' do
user = create(:user)
assert_not(user.confirmed?)
user.confirm!
assert(user.confirmed?)
end
Once again, if you run the tests you should seem them fail. With a failing test in place, we can write the code to make them pass:
class User < ActiveRecord::Base
has_secure_password
validates :email, presence: true, email: true, uniqueness: true
def confirm!
update!(confirmed_at: DateTime.now)
end
def confirmed?
!!confirmed_at
end
end
Here I’ve added two methods to the User
model. The first method confirm!
will update the confirmed_at
column with the current timestamp.
The second method confirmed?
will check to see if the confirmed_at
timestamp is currently nil
.
Now that we’ve added those two methods, if you run the tests again you should seem them all pass.
The final thing we will look at today will be assigning users to different roles.
By default, all users will have the guest
role, but we will also have different roles that will give access to higher levels of authority in the application.
This is something that is a very common requirement in Software as a Service types applications.
We’ve previously looked at adding this functionality in Implementing Roles and Permissions in Ruby on Rails, but we will cover it again here.
The first thing I will do will be to use the Rails Generators to create the model, migration and test file for a new model called Role
. Run the following command in Terminal:
bin/rails g model Role name:string
Next open up the migration file:
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name
t.timestamps null: false
end
end
end
Once again I’m going to add some database constraints to this table. Update the migration file to look like this:
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name, unique: true, null: false
t.timestamps null: false
end
end
end
A “role” should have a name, and that name should be unique because the value of the role is it’s name.
To assert these business rules, we can add the following two Shoulda assertions to the role_test.rb
under the test/models
directory:
require 'test_helper'
class RoleTest < ActiveSupport::TestCase
should validate_presence_of(:name)
should validate_uniqueness_of(:name)
end
If you run the following command in Terminal, you will see those two tests fail:
bin/rake test test/models/role_test.rb
With the failing tests in place, we can add the code to make them pass:
class Role < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
end
As you might of guessed, in order to make those tests pass we simply need to add some validation rules to the Role
Model.
Now if you run those tests again, you should see them pass.
The next thing we need to do is to create the Assignment
Model that will sit between the User
and the Role
Models. This is your classic “has many through” relationship.
So first up we can use the Rails Generator again:
bin/rails g model Assignment user:references role:references
This will create the usual suspects as we’ve seen a couple of times now.
If we take a peak into the assignments migration file we will see the following:
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.references :user, index: true, foreign_key: true
t.references :role, index: true, foreign_key: true
t.timestamps null: false
end
end
end
As you can see, because we used the generator, Rails was clever enough to set everything up for us.
The Assignment
model looks like this:
class Assignment < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
We can assert that this is working correctly by adding the following two Shoulda assertions to the assignment_test.rb
file under the test/models
directory:
require 'test_helper'
class AssignmentTest < ActiveSupport::TestCase
should belong_to(:user)
should belong_to(:role)
end
If you run the following command in Terminal you will see those tests pass because Rails has already added the code for these associations for us.
We will also need to add the associations to the User
and Role
Models.
The User
Model should look like this:
class User < ActiveRecord::Base
has_secure_password
has_many :assignments
has_many :roles, through: :assignments
validates :email, presence: true, email: true, uniqueness: true
def confirm!
update!(confirmed_at: DateTime.now)
end
def confirmed?
!confirmed_at.nil?
end
end
And the Role
Model should look like this:
class Role < ActiveRecord::Base
has_many :assignments
has_many :users, through: :assignments
validates :name, presence: true, uniqueness: true
end
We can also add the following assertions to the User
and Role
test files.
The user_test.rb
file will look like this:
class UserTest < ActiveSupport::TestCase
should have_many(:assignments)
should have_many(:roles).through(:assignments)
# The rest of the tests have been left out for brevity
end
Finally we can add another test to the user_test.rb
file to assert that a user has a role:
test 'user should have role' do
assert_not(@subject.role? :admin)
@subject.roles << Role.new(name: 'admin')
assert(@subject.role? :admin)
end
If you run this test you should see it fail. With a failing test in place we can add the following method to the user.rb
model file under app/models
:
def role?(role)
roles.any? { |r| r.name.underscore.to_sym == role }
end
Now if you run those tests again, you should see them all pass!
We’ve covered quite a lot of the core functionality of the User
model in today’s tutorial.
I think a lot of web applications will start from these foundations, and so hopefully today’s tutorial was a good walkthrough of your first steps towards building a Rails application.
In the mean time, you can take a look at the code for this tutorial on Github.