Skip to content

Testing with RSpec — Behavior-Driven Testing in Rails

DodaTech Updated 2026-06-28 4 min read

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

Should I use RSpec or Minitest?

RSpec has more expressive syntax. Minitest is simpler and faster. Both work well.

What is the difference between stub and mock?

A stub replaces a method with a return value. A mock expects a method call.

How do I run a single spec file?

bundle exec rspec spec/models/product_spec.rb.

What is the rails_helper vs spec_helper?

rails_helper loads Rails. spec_helper is for non-Rails specs.

How do I test background jobs?

Use have_enqueued_job matcher or perform_enqueued_jobs helper.

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