RoR Testing Basics
Testing confirms your code works correctly and keeps working as your app grows. RSpec is the most popular testing framework for Ruby and Rails. It lets you describe expected behavior in plain English and then verify it automatically. A good test suite catches bugs before users do.
Why Tests Matter
Without tests: You change code → manually click through the app → hope nothing broke With tests: You change code → run one command → tests tell you exactly what broke
Tests also act as documentation. A well-written test shows exactly how a piece of code is supposed to behave.
Install RSpec
Add RSpec to your Gemfile inside the test and development groups:
group :development, :test do gem "rspec-rails" gem "factory_bot_rails" gem "faker" end
bundle install rails generate rspec:install
This creates:
spec/ ← all test files live here spec/spec_helper.rb ← RSpec configuration spec/rails_helper.rb ← Rails-specific RSpec settings .rspec ← default RSpec flags
RSpec Vocabulary
| Term | Meaning | Example |
|---|---|---|
| describe | Groups tests for a class or method | describe User do |
| context | Groups tests for a specific situation | context "when admin" do |
| it | One individual test case | it "validates email presence" |
| expect | What you assert should happen | expect(user).to be_valid |
| to / not_to | Passes or fails based on the matcher | expect(count).to eq(3) |
| before | Code that runs before each test | before { @user = User.new } |
| let | Lazy-evaluated variable shared in a describe block | let(:user) { User.new } |
Your First RSpec Test
spec/models/user_spec.rb
require "rails_helper"
RSpec.describe User, type: :model do
describe "validations" do
it "is valid with a name and email" do
user = User.new(name: "Alice", email: "alice@example.com")
expect(user).to be_valid
end
it "is invalid without a name" do
user = User.new(email: "alice@example.com")
expect(user).not_to be_valid
expect(user.errors[:name]).to include("can't be blank")
end
it "is invalid without an email" do
user = User.new(name: "Alice")
expect(user).not_to be_valid
end
it "is invalid with a duplicate email" do
User.create(name: "Alice", email: "alice@example.com")
duplicate = User.new(name: "Bob", email: "alice@example.com")
expect(duplicate).not_to be_valid
end
end
end
Running Tests
Run all specs: bundle exec rspec Run one file: bundle exec rspec spec/models/user_spec.rb Run one specific test: bundle exec rspec spec/models/user_spec.rb:12 Run specs with detailed output: bundle exec rspec --format documentation
Test output:
User
validations
is valid with a name and email
is invalid without a name
is invalid without an email
is invalid with a duplicate email
4 examples, 0 failures
Common Matchers
Equality:
expect(value).to eq(5)
expect(name).to eq("Alice")
Truthiness:
expect(user).to be_valid
expect(user).to be_admin
expect(list).to be_empty
expect(list).not_to be_empty
Inclusion:
expect(errors).to include("can't be blank")
expect(array).to include(3)
Count / Change:
expect { User.create(...) }.to change(User, :count).by(1)
expect { user.destroy }.to change(User, :count).by(-1)
Raises error:
expect { User.find(999) }.to raise_error(ActiveRecord::RecordNotFound)
Output / Return:
expect(user.full_name).to eq("Alice Smith")
expect(article.status).to eq("published")
Using let and before
RSpec.describe Article, type: :model do
let(:user) { User.create(name: "Alice", email: "alice@example.com") }
let(:article) { Article.new(title: "Hello", body: "World", user: user) }
describe "#published?" do
context "when published is true" do
before { article.published = true }
it "returns true" do
expect(article.published?).to be true
end
end
context "when published is false" do
before { article.published = false }
it "returns false" do
expect(article.published?).to be false
end
end
end
end
FactoryBot — Create Test Data Easily
Writing User.new(name: "Alice", email: "...") in every test is tedious. FactoryBot provides reusable templates:
spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.full_name }
email { Faker::Internet.unique.email }
password { "password123" }
role { :viewer }
end
end
spec/factories/articles.rb
FactoryBot.define do
factory :article do
title { Faker::Lorem.sentence(word_count: 4) }
body { Faker::Lorem.paragraphs(number: 3).join("\n") }
published { false }
association :user
end
end
Use factories in tests:
user = create(:user) ← saves to test database article = create(:article, user: user) admin = create(:user, role: :admin) draft = create(:article, published: false) user = build(:user) ← not saved (faster)
Test Structure Best Practices
spec/
models/
user_spec.rb
article_spec.rb
controllers/
articles_controller_spec.rb
requests/
articles_spec.rb ← integration tests
factories/
users.rb
articles.rb
support/
factory_bot.rb
The Testing Pyramid
/\
/ \
/ UI \ ← few, slow, high-level
/------\
/ Request\ ← moderate number
/----------\
/ Model \ ← many, fast, low-level
/--------------\
Write the most unit tests (model tests) — they run fast.
Write fewer integration tests — they test real user flows.
Write minimal UI tests — they are slow and brittle.
Tests written before you write the actual code is called Test-Driven Development (TDD). Many experienced Rails developers write the test first, watch it fail, then write just enough code to make it pass.
