Ruby

RSpec Tests in Ruby on Rails: Every Spec Type Explained (2026)

RSpec Tests in Ruby on Rails: Every Spec Type Explained (2026)

This post is part of the Behavior Testing in Ruby with RSpec series

  1. 1Behaviour Driven Development in Ruby with RSpec
  2. 2RSpec Tests in Ruby on Rails: Every Spec Type Explained (2026)

RSpec is the most widely used testing framework for Ruby on Rails. With the rspec-rails gem (v8.x, supporting Rails 7.2–8.x), you write model specs for business logic, request specs for controllers and APIs, and system specs for browser flows — plus mailer and job specs. Run the suite with bundle exec rspec.

In the first part of this series, we looked at the basics of RSpec and how well it works with Behavior Driven Development in Ruby.

In this part, we’ll set up rspec-rails in a Rails 8 application and walk through each spec type with runnable examples.

Unit, Controller, and Integration Tests in Ruby on Rails

A Ruby on Rails application is composed of several layers. The framework is built around models, views, and controllers, but that barely covers a real application. Mailers, jobs, and helpers are secondary layers you don’t want to miss.

How testable these layers are depends on how you design your code. Following SOLID principles, particularly Single Responsibility, keeps classes small and straightforward to test. Test each brick of code on its own, then keep the code in controllers, views, and jobs thin: they should assemble bricks you have already tested. The closer you get to the browser, the more complex and slow testing becomes.

Categories of Ruby Tests

RSpec organizes Rails tests into spec types, one per layer. Here is what each covers, and how much of it you should write:

  • Model specs (unit tests): Cover your models’ public methods and any plain old Ruby objects (POROs). Write many of these, keep them focused, and avoid database access where you can. They should run fast.
  • Request specs: Cover the controller layer from a machine client’s perspective: “for a given HTTP verb, path, and parameters, what response should we get?” They are the standard way to test controllers and APIs.
  • System specs: Simulate a human user in a browser with Capybara. Reserve them for scenarios that matter, like a signup or checkout flow. They exercise the whole stack, so they are slow.
  • Mailer and job specs: Confirm that emails carry the right headers and content, and that jobs get enqueued when they should. The heavy lifting behind both belongs in classes with their own unit tests.
  • Helper, view, and routing specs: Rarely needed. Reach for them when a helper grows complex, a view fragment carries logic, or your routing does. Complex routing is a code smell in itself.

Writing every possible spec type for every change causes fatigue and a slow suite for little benefit. Cover each layer once, in the cheapest spec type that can catch the regression.

First, install RSpec.

Using RSpec and Ruby on Rails for Testing

The rspec-rails gem integrates RSpec into a Rails application. Add it to the :development and :test groups in your Gemfile:

Ruby
# Gemfile
group :development, :test do
  gem "rspec-rails", "~> 8.0"
end

Then install the gem and run the install generator:

Shell
bundle install
bin/rails generate rspec:install

The generator creates the pieces RSpec needs in a Rails context:

Shell
create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

spec_helper.rb configures RSpec itself, while rails_helper.rb loads the Rails environment on top. Specs that touch Rails require rails_helper; specs for plain Ruby classes can require the lighter spec_helper instead.

A note on versions: we verified everything in this post with rspec-rails 8.0.4 on Rails 8.1 and Ruby 3.4. The gem requires Rails 7.2 or later, so version 8.x covers Rails 7.2 through 8.x. Since version 7, rspec-rails versions independently of RSpec itself: the 8.x series pulls in the current 3.13 line of rspec-core, rspec-expectations, and rspec-mocks.

From here on, one command runs your whole suite:

Shell
bundle exec rspec

What Types of Specs Does rspec-rails Support?

rspec-rails hooks into the Rails generators, so scaffolding a model or controller also scaffolds the matching spec. You can generate each spec type directly too:

Spec typeWhat it testsSpeedGenerator command
ModelBusiness logic, validations, POROsFastbin/rails generate rspec:model
RequestController actions, routing, APIs over HTTPFastbin/rails generate rspec:request
SystemFull user flows in a (headless) browserSlowbin/rails generate rspec:system
MailerEmail headers and body contentFastbin/rails generate rspec:mailer
JobJob enqueueing and behaviorFastbin/rails generate rspec:job
HelperView helper methodsFastbin/rails generate rspec:helper
ViewRendered view fragmentsMediumbin/rails generate rspec:view
Controller (legacy)Single controller actions in isolationMediumbin/rails generate rspec:controller

Request specs supersede controller specs. Controller specs remain available for legacy suites, but both the Rails and RSpec teams recommend request specs instead, and helpers like assigns now live in the separate rails-controller-testing gem. Write request specs for new code.

Unit Tests

Unit tests sit at the base of the testing pyramid, so a codebase holds many of them. They need to be fast.

Remember: RSpec tests focus on code behavior, not implementation. In a Rails project, models are the biggest source of unit tests. Models talk to the database, and database access is what makes model specs slow. You can’t always avoid create, save, update, or find, but in most examples, new is enough.

Here is a model spec for a Subscription model with a plan_name presence validation and an #active? method:

Ruby
# spec/models/subscription_spec.rb
require "rails_helper"
 
RSpec.describe Subscription, type: :model do
  describe "validations" do
    it "requires a plan name" do
      subscription = Subscription.new(plan_name: nil)
 
      expect(subscription).not_to be_valid
    end
  end
 
  describe "#active?" do
    context "when the subscription is active" do
      subject(:subscription) { Subscription.new(plan_name: "pro", active: true) }
 
      it { expect(subscription.active?).to be(true) }
    end
 
    context "when the subscription is canceled" do
      subject(:subscription) { Subscription.new(plan_name: "pro", active: false) }
 
      it "returns false" do
        expect(subscription.active?).to be(false)
      end
    end
  end
end

The two #active? examples do the same kind of thing in two styles. The one-liner has no description and still reads nicely; the expanded form carries a description at the cost of some duplication. Pick one style per context and stay consistent.

We use Subscription.new throughout. Nothing here reads from or writes to the database, so these examples run in microseconds. That’s what a good unit test looks like: focused, fast, and independent.

If you write many validation and association examples, the shoulda-matchers gem collapses them into one-liners — we covered it in how to use shoulda-matchers with RSpec for Ruby on Rails.

Unit tests aren’t only for models. Any PORO you build around your models deserves them too. Drop the type: :model metadata and require spec_helper instead of rails_helper, and the same style applies to plain Ruby classes.

Controller and Request Specs

Until a few years ago, controller specs were the main way to test controllers. Today, request specs have replaced them. Request specs test your application from a machine client’s perspective, through the full stack: routing, middleware, controller, and view.

A request spec defines a context made of an HTTP verb, a path, and (optionally) parameters and headers. The expectation then targets the response: its status code, headers, and body.

The simplest request spec checks that an endpoint responds:

Ruby
# spec/requests/subscriptions_spec.rb
require "rails_helper"
 
RSpec.describe "Subscriptions", type: :request do
  describe "GET /subscriptions" do
    it "responds with a successful status" do
      get "/subscriptions"
 
      expect(response).to have_http_status(:ok)
    end
  end
end

The context lives in the line doing the request: get "/subscriptions". The expectation centers on response, an object RSpec exposes for exactly this purpose.

Request specs shine when testing API endpoints. Here we post JSON to create a subscription, then check both the happy path and a validation failure:

Ruby
# spec/requests/subscriptions_spec.rb
require "rails_helper"
 
RSpec.describe "Subscriptions", type: :request do
  describe "POST /subscriptions" do
    it "creates a subscription and returns its details as JSON" do
      post "/subscriptions",
        params: { subscription: { plan_name: "pro", active: true } },
        as: :json
 
      expect(response).to have_http_status(:created)
      expect(response.parsed_body["plan_name"]).to eq("pro")
    end
 
    it "rejects a subscription without a plan name" do
      post "/subscriptions",
        params: { subscription: { plan_name: "", active: true } },
        as: :json
 
      expect(response).to have_http_status(:unprocessable_entity)
    end
  end
end

as: :json sets the content type and encodes the parameters, and response.parsed_body decodes the JSON response for you. The full RSpec vocabulary of describe, context, and let still applies; request specs only add helpers for the HTTP layer.

System Specs

System specs are the most complete integration tests. They drive your application in a real or headless browser through Capybara, which needs a driver. The default driver is Selenium; driven_by changes it per spec or suite-wide.

Where request specs speak HTTP, system specs speak user: you visit pages, fill in forms, and click buttons. Here is a system spec for a signup flow, using the :rack_test driver, which needs no browser at all:

Ruby
# spec/system/subscriptions_spec.rb
require "rails_helper"
 
RSpec.describe "Subscription signup", type: :system do
  before do
    driven_by :rack_test
  end
 
  it "lets a customer pick a plan" do
    visit "/subscriptions/new"
 
    fill_in "Plan name", with: "pro"
    click_button "Create Subscription"
 
    expect(page).to have_text("Subscription created.")
  end
end

The HTTP context is still there, underneath: visit makes a GET request, filling the form sets parameters, and clicking the button triggers a POST. The expectation reads the rendered page rather than a raw response body.

The :rack_test driver is fast but doesn’t execute JavaScript. When a flow depends on JavaScript, switch that spec to a real browser with driven_by :selenium, using: :headless_chrome.

Because system specs drive the complete stack, they run far slower than other specs. Keep them few, reserve them for your most valuable flows, and consider running them in a separate CI step.

Complementary Test Types in Ruby

Beyond models, controllers, and browser flows, Rails applications carry components with specific roles: mailers, jobs, serializers, and decorators. These need tests closer to unit tests than integration tests, and rspec-rails provides dedicated spec types for the first two.

Mailer Specs

Action Mailer abstracts the delivery of email away from your code, so you don’t need to test that emails get sent. Instead, test the behavior that’s yours: the subject, the from and to addresses, and the body.

Ruby
# spec/mailers/renewal_mailer_spec.rb
require "rails_helper"
 
RSpec.describe RenewalMailer, type: :mailer do
  describe "reminder" do
    let(:mail) { RenewalMailer.reminder }
 
    it "prepares the email headers" do
      expect(mail.subject).to eq("Your subscription renews soon")
      expect(mail.to).to eq(["customer@example.org"])
      expect(mail.from).to eq(["billing@example.com"])
    end
 
    it "renders the body" do
      expect(mail.body.encoded).to match("renews in seven days")
    end
  end
end

Calling the mailer method returns the mail object without delivering anything, so these specs stay fast.

Job Specs

Job specs cover the queueing behavior of your background jobs: whether a job gets enqueued, on which queue, and with which arguments. Active Job already has its own tests; you don’t need to prove that perform_later enqueues. What you want to know is whether your code enqueues the right job at the right time.

Set the queue adapter to :test and use the matchers rspec-rails provides:

Ruby
# spec/jobs/subscription_sync_job_spec.rb
require "rails_helper"
 
RSpec.describe SubscriptionSyncJob, type: :job do
  before do
    ActiveJob::Base.queue_adapter = :test
  end
 
  it "enqueues the job on the billing queue" do
    expect {
      SubscriptionSyncJob.perform_later(42)
    }.to have_enqueued_job(SubscriptionSyncJob).with(42).on_queue("billing")
  end
 
  it "records the job as enqueued exactly once" do
    SubscriptionSyncJob.perform_later(42)
 
    expect(SubscriptionSyncJob).to have_been_enqueued.with(42).exactly(:once)
  end
end

have_enqueued_job wraps a block and checks what the block enqueues; have_been_enqueued checks after the fact. Keep the job’s perform method thin: it should call classes covered by their own unit tests. And once those jobs run in production, AppSignal instruments background jobs for Sidekiq, Solid Queue, and friends, so you can watch queue times and failures per job class.

A Note on Concerns in Ruby on Rails

Rails lets you share code across models or controllers through a disguised Ruby module called a concern. Test a concern’s behavior through what includes it: model specs cover concerns in models, while request and system specs cover concerns in controllers.

Some Thoughts on Testing with RSpec for Ruby

Your first layer of testing should be unit tests. Model and PORO specs should represent the vast majority of specs in a codebase.

Specs should run fast and avoid database and external service access as much as possible. To test the controller layer, use request specs for machine-driven activity and system specs for human-driven flows. Both cost more than unit tests, so keep them fewer in number.

Finally, keep mailer and job specs in their specific lane: test your headers, bodies, and queueing decisions, not the behavior of Action Mailer or Active Job themselves.

A green suite doesn’t guarantee a fast app — and a slow, flaky suite often mirrors slow, flaky production code. AppSignal’s Ruby monitoring traces the requests and background jobs your specs exercise, so you can see how they actually behave after deploy.

Wrapping Up

As we saw in part one of this series, RSpec’s building blocks (before hooks, let, describe, and context) help you avoid duplication across all of these spec types. Keeping unit tests small comes naturally; keeping request and system specs slim takes work. After your specs turn green, spend some time trimming them down. Future you will thank you when the suite stays readable and fast.

Happy testing!

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

This post is part of the Behavior Testing in Ruby with RSpec series

  1. 1Behaviour Driven Development in Ruby with RSpec
  2. 2RSpec Tests in Ruby on Rails: Every Spec Type Explained (2026)

Frequently asked questions

How do I install rspec-rails in Rails 8?
Add the rspec-rails gem to the development and test groups in your Gemfile, run bundle install, then run rails generate rspec:install. The generator creates the .rspec file plus spec/spec_helper.rb and spec/rails_helper.rb. rspec-rails 8 supports Rails 7.2 and later.
What is the difference between request specs and system specs in RSpec?
Request specs send HTTP requests through the full Rails stack without a browser, so they run fast and suit APIs. System specs drive a browser through Capybara, so they can cover JavaScript and multi-step user flows, but they run much slower.
What types of specs does rspec-rails support?
rspec-rails ships generators for model, request, system, mailer, job, helper, view, routing, controller, and feature specs. Most Rails suites rely on three of them: model specs for business logic, request specs for controllers and APIs, and a small set of system specs.
Should I use controller specs or request specs in Rails?
Use request specs. The Rails and RSpec teams both recommend them over controller specs, which need the separate rails-controller-testing gem for older helpers. Request specs run through routing and middleware, so they test what production traffic hits.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
Thomas Riboulet

Thomas Riboulet

Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.

All articles by Thomas Riboulet

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal