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

TermMeaningExample
describeGroups tests for a class or methoddescribe User do
contextGroups tests for a specific situationcontext "when admin" do
itOne individual test caseit "validates email presence"
expectWhat you assert should happenexpect(user).to be_valid
to / not_toPasses or fails based on the matcherexpect(count).to eq(3)
beforeCode that runs before each testbefore { @user = User.new }
letLazy-evaluated variable shared in a describe blocklet(: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.

Leave a Comment

Your email address will not be published. Required fields are marked *