RoR Controller Tests
Controller tests verify that your application responds correctly to HTTP requests. They check that the right template renders, the right redirect happens, correct flash messages appear, and that authentication is enforced. In modern Rails, request specs (which test the full request-response cycle) are preferred over isolated controller specs.
Request Specs vs Controller Specs
Controller Specs (older style): Test the controller in isolation Use: spec/controllers/ Request Specs (modern Rails standard): Test the full HTTP request → router → controller → response cycle Use: spec/requests/ Recommended by Rails and RSpec teams
This topic covers request specs, which are the current standard.
Basic Request Spec Structure
spec/requests/articles_spec.rb
require "rails_helper"
RSpec.describe "Articles", type: :request do
describe "GET /articles" do
it "returns a successful response" do
get articles_path
expect(response).to have_http_status(:ok)
end
it "renders the index template" do
get articles_path
expect(response).to render_template(:index)
end
end
describe "GET /articles/:id" do
let(:article) { create(:article) }
it "returns a successful response" do
get article_path(article)
expect(response).to have_http_status(:ok)
end
it "returns 404 for a non-existent article" do
get article_path(id: 99999)
expect(response).to have_http_status(:not_found)
end
end
end
Testing Create Actions
describe "POST /articles" do
context "with valid parameters" do
let(:valid_attrs) { { article: { title: "New Post", body: "Body text here." } } }
it "creates a new article" do
expect {
post articles_path, params: valid_attrs
}.to change(Article, :count).by(1)
end
it "redirects to the new article" do
post articles_path, params: valid_attrs
expect(response).to redirect_to(article_path(Article.last))
end
it "sets a success flash message" do
post articles_path, params: valid_attrs
expect(flash[:notice]).to be_present
end
end
context "with invalid parameters" do
let(:invalid_attrs) { { article: { title: "", body: "" } } }
it "does not create a new article" do
expect {
post articles_path, params: invalid_attrs
}.not_to change(Article, :count)
end
it "renders the new template" do
post articles_path, params: invalid_attrs
expect(response).to render_template(:new)
end
it "returns unprocessable entity status" do
post articles_path, params: invalid_attrs
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
Testing Authentication
Use a helper to log in as a user in your tests. With Devise, use the devise test helpers:
spec/rails_helper.rb RSpec.configure do |config| config.include Devise::Test::IntegrationHelpers, type: :request end
spec/requests/articles_spec.rb
describe "POST /articles" do
context "when not logged in" do
it "redirects to the login page" do
post articles_path, params: { article: { title: "Test" } }
expect(response).to redirect_to(new_user_session_path)
end
end
context "when logged in as a regular user" do
let(:user) { create(:user) }
before { sign_in user }
it "allows creating an article" do
expect {
post articles_path, params: { article: { title: "Test", body: "Body" } }
}.to change(Article, :count).by(1)
end
end
context "when logged in as a viewer (no permission)" do
let(:viewer) { create(:user, role: :viewer) }
before { sign_in viewer }
it "redirects with an alert" do
post articles_path, params: { article: { title: "Test", body: "Body" } }
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to be_present
end
end
end
Testing Update and Delete
describe "PATCH /articles/:id" do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
before { sign_in user }
context "with valid parameters" do
it "updates the article" do
patch article_path(article), params: { article: { title: "Updated Title" } }
expect(article.reload.title).to eq("Updated Title")
end
it "redirects to the article" do
patch article_path(article), params: { article: { title: "Updated" } }
expect(response).to redirect_to(article_path(article))
end
end
context "when another user tries to edit" do
let(:other_user) { create(:user) }
before { sign_in other_user }
it "denies access" do
patch article_path(article), params: { article: { title: "Hack" } }
expect(response).to redirect_to(root_path)
end
end
end
describe "DELETE /articles/:id" do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
before { sign_in user }
it "deletes the article" do
expect {
delete article_path(article)
}.to change(Article, :count).by(-1)
end
it "redirects to the articles list" do
delete article_path(article)
expect(response).to redirect_to(articles_path)
end
end
Testing JSON API Endpoints
describe "GET /api/v1/articles" do
before { create_list(:article, 3, published: true) }
it "returns JSON with all articles" do
get api_v1_articles_path,
headers: { "Accept" => "application/json" }
expect(response).to have_http_status(:ok)
expect(response.content_type).to include("application/json")
json = JSON.parse(response.body)
expect(json.length).to eq(3)
expect(json.first).to have_key("title")
end
end
HTTP Status Matchers
expect(response).to have_http_status(:ok) # 200 expect(response).to have_http_status(:created) # 201 expect(response).to have_http_status(:no_content) # 204 expect(response).to have_http_status(:unauthorized) # 401 expect(response).to have_http_status(:not_found) # 404 expect(response).to have_http_status(:redirect) # 3xx any expect(response).to redirect_to(articles_path) # specific URL expect(response).to render_template(:index) # specific template
What Request Specs Cover
Request Spec checks: ✓ HTTP status code returned ✓ Template rendered (or redirect destination) ✓ Flash message set ✓ Database record created/updated/deleted ✓ Authentication enforced ✓ Authorization enforced ✓ JSON response structure and content
Request specs test your entire application stack from router to response. They catch integration bugs that model tests cannot — like a controller using the wrong template, missing authentication guards, or incorrect redirects after form submission.
