Testing with RSpec — Behavior-Driven Testing in Rails
In this tutorial, you will learn about Testing with RSpec. We cover key concepts, practical examples, and best practices to help you master this topic.
RSpec provides behavior-driven testing for Rails with model specs, request specs, system specs, feature specs, shared examples, mocking, and expressive matchers.
What You'll Learn
By the end of this tutorial, you'll write model and request specs, use shared examples, implement mocking with RSpec mocks, write system specs with Capybara, and practice TDD.
Why RSpec Matters
RSpec's expressive syntax describes behavior rather than implementation. Its matchers, shared examples, and mocking make tests readable and maintainable.
Real-World Use
A team uses RSpec with Factory Bot. Model specs validate business logic. Request specs test API endpoints. System specs verify browser interactions with Capybara.
Testing Path
flowchart LR
A[Rails Testing] --> B[RSpec]
B --> C[Model Specs]
B --> D[Request Specs]
B --> E[System Specs]
B --> F[Shared Examples]
B --> G{You Are Here}
style G fill:#f90,color:#fff
Model Specs
Test model validations, scopes, and methods.
# spec/models/product_spec.rb
require "rails_helper"
RSpec.describe Product, type: :model do
describe "validations" do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:price) }
it { should validate_numericality_of(:price).is_greater_than_or_equal_to(0) }
it { should belong_to(:category).optional }
it { should have_many(:reviews).dependent(:destroy) }
end
describe "scopes" do
let!(:published_product) { create(:product, published: true) }
let!(:draft_product) { create(:product, published: false) }
it "returns only published products" do
expect(Product.published).to contain_exactly(published_product)
end
end
describe "#on_sale?" do
it "returns true when sale_price is set" do
product = build(:product, sale_price: 10.0)
expect(product).to be_on_sale
end
it "returns false without sale_price" do
product = build(:product, sale_price: nil)
expect(product).not_to be_on_sale
end
end
end
Request Specs
Test HTTP endpoints.
# spec/requests/api/v1/products_spec.rb
require "rails_helper"
RSpec.describe "Api::V1::Products", type: :request do
describe "GET /api/v1/products" do
let!(:products) { create_list(:product, 3, published: true) }
it "returns a successful response" do
get api_v1_products_path
expect(response).to have_http_status(:ok)
end
it "returns all published products" do
get api_v1_products_path
json = response.parsed_body
expect(json["data"].size).to eq(3)
end
it "includes product attributes" do
get api_v1_products_path
product = response.parsed_body["data"].first
expect(product["attributes"]).to include("name", "price")
end
end
describe "POST /api/v1/products" do
let(:valid_params) { { product: { name: "New Product", price: 19.99 } } }
context "when authenticated" do
before { sign_in create(:user) }
it "creates a product" do
expect {
post api_v1_products_path, params: valid_params
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
end
context "when unauthenticated" do
it "returns unauthorized" do
post api_v1_products_path, params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
end
end
System Specs
Test full browser interactions with Capybara.
# spec/system/product_management_spec.rb
require "rails_helper"
RSpec.describe "Product Management", type: :system do
before do
driven_by(:rack_test)
end
let(:admin) { create(:user, :admin) }
it "allows admin to create a product" do
sign_in admin
visit new_product_path
fill_in "Name", with: "Widget"
fill_in "Price", with: "19.99"
click_button "Create Product"
expect(page).to have_content("Product was successfully created")
expect(page).to have_content("Widget")
end
it "shows validation errors" do
sign_in admin
visit new_product_path
click_button "Create Product"
expect(page).to have_content("can't be blank")
end
end
Common Mistakes
1. Testing Implementation Instead of Behavior
Tests should verify outcomes, not internal methods. Prefer request specs for API testing.
2. Slow Test Suites
Inefficient factories, unnecessary database writes, and system tests slow down suites.
3. Not Using let for Shared Setup
Instance variables in before blocks are slower. Use let for lazy evaluation.
4. Over-Mocking
Mocking everything hides real integration issues. Use real objects when practical.
5. Ignoring Test Fixtures
Factory Bot creates test data. Use traits and sequences for maintainable factories.
Practice Questions
1. What is the difference between request spec and system spec?
Request specs test HTTP responses. System specs test browser interactions.
2. What does let do?
Defines a memoized helper method that loads lazily within the example.
3. How do you test JSON responses?
Use response.parsed_body to access the parsed JSON.
4. What is a shared example?
A reusable set of tests that can be included in multiple specs.
5. Challenge: Write a request spec for creating a product with authentication.
RSpec.describe "Products", type: :request do
let(:user) { create(:user) }
let(:product_params) { { product: { name: "Widget", price: 9.99 } } }
it "creates product when authenticated" do
sign_in user
expect {
post products_path, params: product_params
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
it "rejects unauthenticated requests" do
post products_path, params: product_params
expect(response).to have_http_status(:unauthorized)
end
end
FAQ
Mini Project: Complete Test Suite
Write a comprehensive test suite for a blog.
RSpec.describe Post, type: :model do
it { should validate_presence_of(:title) }
it { should belong_to(:author) }
it { should have_many(:comments).dependent(:destroy) }
describe "#published?" do
it "returns true when published_at is set and in past" do
post = build(:post, published_at: 1.hour.ago)
expect(post).to be_published
end
end
end
What's Next
Rails Factory Bot Rails Faker Rails Shoulda Matchers
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro