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.

Leave a Comment

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