cult3

Creating a sign up form flow in Ruby on Rails Part 1

Mar 23, 2016

Table of contents:

  1. How is this going to work?
  2. Creating a new Rails project
  3. Generating the User Model
  4. Adding some Gems for testing
  5. Writing the first Unit Test for the User Model
  6. Asserting that the email is valid
  7. Asserting that the email is unique
  8. Asserting that the User is confirmed
  9. Generating the Role Model
  10. Generating the Assignment Model
  11. Conclusion

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.

How is this going to work?

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.

Creating a new Rails project

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.

Generating the User Model

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

Adding some Gems for testing

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.

Writing the first Unit Test for the User Model

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!

Asserting that the email is valid

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!

Asserting that the email is unique

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.

Asserting that the User is confirmed

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.

Generating the Role Model

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.

Generating the Assignment Model

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!

Conclusion

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.