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