Skip to the content.

Home

Previous: Chapter 1 - The App


The first thing you’ll notice about this guide is that I’ll take your through vertical slices of the application. It’s my preferred approach and I think it works well with Rails especially. This means that, in contrast to documentation, some areas of the application will be split between chapters. In other words, don’t expect to build every model right now. To start off, we’ll build a User model with name and email attributes. We’ll offload as much work as possible to the generator.

Check out documentation for the generate model command.

Terminal

bin/rails generate model

Skim through the output to see the available options.

Generate a User model.

Terminal

bin/rails generate model user name:string email:string:uniq

Take a look at what was generated for you. We’re off to a great start just by leveraging the magic of Rails, but we want to make some changes of our own. First we’ll check the migration. We’re creating the users table with name and email fields. We’re also adding a unique index on email. There are two benefits to this index. First, it ensures that two users can’t be created with the same email. Second, it can speed up queries that search and filter by email.

We’ll still need to make a few changes. First, we’ll add not-null constraints to name and email. There’s no need for either field to ever be null. The same goes for many fields. If nullable columns are common among your tables, I would think about your design. Second, since emails are case insensitive, our index will need to reflect that. In PostgreSQL, the default collation will be case-sensitive. This means that daniel@example.com and DANIEL@example.com are considered two different emails. To fix this problem, we’ll use an expression index to lower case email data in the index.

Make those changes.

db/migrate/[timestamp]_create_users.rb

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false

      t.timestamps
    end
    add_index :users, "lower(email)", unique: true
  end
end

Run the migration.

Terminal

bin/rails db:migrate

You’ll notice that it automatically updates db/schema.rb. Now that we’ve handled our database, we want to enforce the same constraints in our application code. We want to enforce it in the User model, to be exact. The way we do this is with model validations. We want to validate that both name and email are present. We also want to validate that email is unique among all users.

Add the validations.

app/models/user.rb

class User < ApplicationRecord
  validates :name, :email, presence: true
  validates :email, uniqueness: { case_sensitive: false }
end

Rails also generated some fixtures for you. Fixtures offer a way to seed your database with sample data. Fixtures are loaded automatically in the test environment and can be loaded in development as well, using bin/rails db:fixtures:load. An alternative to fixtures, that you might see, are factories.

Add some custom fixtures.

test/fixtures/users.yml

sarah:
  name: Sarah Fleming
  email: sarah@example.com

chantel:
  name: Chantel Miller
  email: chantel@example.com

adrian:
  name: Adrian Lee
  email: adrian@example.com

daniel:
  name: Daniel Bryant
  email: daniel@example.com

Before we write tests, one addition to our Gemfile will be invaluable. That addition will be the shoulda gem. We can follow the installation steps to get set up.

Add shoulda to the Gemfile.

Gemfile (lines omitted)

group :test do
  gem 'shoulda', '~4.0'
end

Make sure to install the new gem.

Terminal

bundle install

Add the configuration.

test/test_helper.rb (lines omitted)

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :minitest
    with.library :rails
  end
end

We’re ready to test! We want to write tests for the constraints and validations that we’ve added. Fortunately shoulda-context and shoulda-matchers make testing validations really easy.

Write a test for every validation in the model.

test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase
  context "validations" do
    should validate_presence_of(:name)
    should validate_presence_of(:email)
    should validate_uniqueness_of(:email).case_insensitive
  end
end

Run the tests.

Terminal

bin/rails t

Success!

✅ Make a commit

Next: Chapter 3 - The Graph