cult3

Creating a Sign Up Form flow in Ruby on Rails Part 2

Mar 30, 2016

Table of contents:

  1. What are we going to build?
  2. Adding the Reform Gem
  3. Setting up the Form Object
  4. Dealing with validation
  5. Dealing with the save process
  6. Conclusion

In last week’s tutorial we set up the Model layer of this registration flow, which included the User, Role, and Assignment models, as well as Unit Tests to assert that the correct business logic was being enforced correctly.

Registration is a unique aspect of building an application that involves processes and rules that only apply when the user is first signing up to your application.

A good way of encapsulating these special types of processes is through Form Objects. We’ve previously looked at Form Objects in Rails in Using Form Objects in Ruby on Rails and Using Form Objects in Ruby on Rails with Reform.

In today’s tutorial we’re going to pick up where we left off last week by adding a Form Object for encapsulating the registration flow. If you missed last week’s post, I would encourage you to read that post before continuing with this post.

What are we going to build?

Before we get into the code, first I will quickly outline what we’re going to be building in today’s tutorial.

The registration process is a unique part of your application in that it will probably have validation rules that only apply in this one circumstance.

For example, you might require that the user confirms their password by typing it twice, or that they have checked the box to agree to your terms and conditions.

You might also want to create additional models when the user is created for an account or subscription. In my case, I want to automatically assign the user to the guest role.

A naive approach to trying to solve this problem is to add this functionality to the User model. But as you can see, we will end up adding extraneous validation rules and additional responsibility to the User Model where it shouldn’t be concerned.

Instead, we can encapsulate this functionality in a Form Object. The Form Object will deal with the business rules of registering as a new user, and will handle creating any additional models and relationships. This will also make it very easy to come back to this process at some point in the future because it will be immediately obvious where the responsibility for the registration process lies.

So with that brief introduction out of the way, lets get down to building the registration Form Object!

Adding the Reform Gem

As I mentioned in Using Form Objects in Ruby on Rails with Reform, I prefer to use Reform as the base of my Form Objects instead of rolling my own. I tend to find that Open Source projects have already dealt with the edge-cases I’m inevitably going to stumble across at some point in the future.

So the first thing to do is to add Reform to the project. Add the following two Gems to your Gemfile:

gem 'reform'
gem 'reform-rails'

And then run the following command in Terminal to install them:

bundle install

Setting up the Form Object

Next we need to add the Form Object and the associated Unit Test files.

Create a new directory called forms under app and then create a new file called register_form.rb:

class RegisterForm < Reform::Form
end

As you can see, this class should inherit from Reform::Form.

Next create a new directory called forms under the test director and then create a new file called register_form_test.rb:

require 'test_helper'

class RegisterFormTest < ActiveSupport::TestCase
  def setup
    @user = User.new
    @form = RegisterForm.new(@user)
  end
end

Dealing with validation

The first responsibility of the register form is to ensure that the correct properties have been sent via the request. In this example we’re only going to be requiring an email address and a password.

So first up we can write a test to ensure that those properties are required:

require 'test_helper'

class RegisterFormTest < ActiveSupport::TestCase
  def setup
    @user = User.new
    @form = RegisterForm.new(@user)
  end

  test 'email should be required' do
    @form.validate({})

    assert_includes(@form.errors[:email], "can't be blank")
  end

  test 'password should be required' do
    @form.validate({})

    assert_includes(@form.errors[:password], "can't be blank")
  end
end

Now if you run the following command in Terminal, you will see both of those tests fail:

bin/rake test test/forms/register_form_test.rb

To make these tests pass, we can tell the RegisterForm class to require these properties:

class RegisterForm < Reform::Form
  property :email
  property :password

  validates :email, presence: true
  validates :password, presence: true
end

Here I’ve added the two properties to the form and then I’ve added validation rules to both to ensure that they are both required.

Now if you run those tests again, you should see them pass.

Next we need to assert that the value that has been provided for the email property is in fact a valid email address.

To make this assertion, we can add the following test:

test 'email should be a valid email address' do
  @form.validate(email: 'invalid')

  assert_includes(@form.errors[:email], 'is not an email')
end

And once again, we can run this test to watch it fail. With a failing test in place, we can add the validation rule to make it pass:

class RegisterForm < Reform::Form
  property :email
  property :password

  validates :email, presence: true, email: true
  validates :password, presence: true
end

Here I’m using the email validation rule that we wrote in last weeks tutorial.

And finally we can add a test to assert that the email address that has been provided is unique:

test 'email should be unique' do
  create(:user, email: 'name@domain.com')

  @form.validate(email: 'name@domain.com')

  assert_includes(@form.errors[:email], 'has already been taken')
end

As you can see, first I create a new user in the database with a given email address by using the Factory Girl create method.

Next I pass the same email into the form and then assert that the correct error has been set.

To make this test pass we can add the unique validation rule to the Form Object:

require 'reform/form/validation/unique_validator.rb'

class RegisterForm < Reform::Form
  property :email
  property :password

  validates :email, presence: true, email: true, unique: true
  validates :password, presence: true
end

Notice how I’m requiring Reform’s unique validator? This provides a better way to check for uniqueness than the default Active Record method.

Dealing with the save process

Now that we’ve got those basic validation rules out of the way, we can turn our attention to the actual process of saving the new user.

Normally if you didn’t have to deal with any other business rules you would be done with the Form Object. But in this case, I want to automatically assign the user to the guest role.

A good way of dealing with this is to override the save method on the form to provide our own custom behaviour.

But first, we will write a test that asserts the behaviour we’re looking for:

test 'should create new unconfirmed guest user' do
  @form.validate(email: 'name@domain.com', password: 'password')

  assert(@form.save)
  assert_not(@user.confirmed?)
end

If you run the tests again, you will see them all pass. By default Reform will take the supplied parameters and call save on the Model for us, therefore everything in this test should pass.

But we also need to assert that the user has a given role. To make this assertion we can add the following extra line to this test:

test 'should create new unconfirmed guest user' do
  @form.validate(email: 'name@domain.com', password: 'password')

  assert(@form.save)
  assert_not(@user.confirmed?)
  assert(@user.role?(:guest), 'user does not have the role of :guest')
end

Here I’m asserting that the user has a role of :guest. If you run these tests again you should see this assertion fail.

With the failing assertion in place we can now add the code to make it pass.

First we can add our own save method to the RegisterForm:

def save
  sync

  model.save
end

In this method first I’m calling the sync method to sync the properties of the form to the model instance.

Next I’m calling save on the model to save the properties.

Here we’ve effectively replicated what was going on in the form before we overide the save method. If you run the tests again you should get the same error as before.

So now we’ve overridden the save method, we now have a place to assign the role to the user:

require 'reform/form/validation/unique_validator.rb'

class RegisterForm < Reform::Form
  property :email
  property :password

  validates :email, presence: true, email: true, unique: true
  validates :password, presence: true

  def save
    sync

    model.roles << Role.find_by(name: :guest)
    model.save
  end
end

Now if you run the tests again, you should see them still fail! The reason why the test is still failing is because we need to seed the database with the guest role.

First create a new file under test/factories called role.rb:

FactoryGirl.define do
  factory :role do
    name Faker::Lorem.word
  end
end

Here I’m just setting the default name of the Role to a random work from Faker.

Next we can create the guest role during the test:

test 'should create new unconfirmed guest user' do
  create(:role, name: 'guest')

  @form.validate(email: 'name@domain.com', password: 'password')

  assert(@form.save)
  assert_not(@user.confirmed?)
  assert(@user.role?(:guest), 'user does not have the role of :guest')
end

Now if you run those tests again you should see them all pass!

Conclusion

In today’s tutorial we’ve encapsulated the registration process in a Form Object. This gives us a single place to deal with specific registration validation, as well as setting up any additional models or associations that will need to be created at the same time.

I find the Form Object pattern a really nice way to encapsulate these important processes.

Alternative methods would be to crowbar this functionality into the User model, or into the Controller, but both of those methods have a lot of negatives with not very much in the way of positives.

Next week we will be continuing with this registration flow mini-series by looking at adding the Controller and View for registering, as well as the Functional Tests to assert everything is working as it should.

You can find the code for this week’s tutorial on Github.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.