Introduction

Controller tests verify that your application responds correctly to HTTP requests. In Rails 5 and later, controller tests are integration-style tests that exercise the full request/response cycle, including routing, middleware, and rendering. Understanding how to write them is essential for ensuring your endpoints behave as users and API consumers expect.

Key Concepts

  • ActionDispatch::IntegrationTest: The base class for all controller tests in modern Rails. It replaces the older ActionController::TestCase and provides a more realistic testing environment.
  • HTTP method helpers: Methods like get, post, patch, put, and delete that simulate browser or API requests against your routes.
  • Response assertions: Matchers such as assert_response and assert_redirected_to that verify the HTTP status code and redirect behavior of a response.
  • Functional vs integration style: Rails 5+ merged functional and integration test styles so that controller tests go through the router, giving you confidence that routes are wired correctly.

Real World Context

Every Rails application exposes endpoints that users interact with through a browser or an API client. Without controller tests, you rely on manual clicking or end-to-end browser tests to confirm that a page loads, a form submission redirects correctly, or an API returns the right status code. Controller tests fill the gap between fast model-level unit tests and slow full-browser system tests, giving you quick feedback on request handling without launching a browser.

Deep Dive

A basic controller test inherits from ActionDispatch::IntegrationTest and uses HTTP method helpers to simulate requests.

Here is a simple test that verifies CRUD actions for an articles resource:

ruby
require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test 'should get index' do
    get articles_url
    assert_response :success
  end

  test 'should get show' do
    article = articles(:first)
    get article_url(article)
    assert_response :success
  end

  test 'should get new' do
    get new_article_url
    assert_response :success
  end
end

In the code above, articles_url and article_url are route helpers generated by resources :articles. The assert_response :success matcher accepts symbols (:success for 2xx, :redirect for 3xx, :not_found for 404) or integer status codes.

For write operations, use assert_difference to verify that the database changed as expected:

ruby
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test 'should create article' do
    assert_difference('Article.count', 1) do
      post articles_url, params: {
        article: { title: 'New', body: 'Content' }
      }
    end
    assert_redirected_to article_url(Article.last)
  end

  test 'should update article' do
    article = articles(:first)
    patch article_url(article), params: {
      article: { title: 'Updated Title' }
    }
    assert_redirected_to article_url(article)
    article.reload
    assert_equal 'Updated Title', article.title
  end

  test 'should destroy article' do
    article = articles(:first)
    assert_difference('Article.count', -1) do
      delete article_url(article)
    end
    assert_redirected_to articles_url
  end
end

Notice that assert_difference takes a string expression and a delta. Pass a negative delta for deletions and a positive one for creations. After an update, call reload on the ActiveRecord object to fetch the latest values from the database.

Rails provides several response assertions you will use frequently:

ruby
assert_response :success      # 200-299
assert_response :redirect     # 300-399
assert_response :not_found    # 404
assert_response 201           # Specific status code

assert_redirected_to articles_url
assert_redirected_to @article

assert_match 'Welcome', response.body

Common Pitfalls

  1. Forgetting to reload after update — After a patch or put, the in-memory ActiveRecord object still holds old values. Call article.reload before asserting on the updated attributes.
  2. Using the wrong URL helper — article_url generates a full URL including host, while article_path generates a relative path. In integration tests, prefer the _url variants because the test runner expects absolute URLs.
  3. Not wrapping state-changing requests in assert_difference — Simply checking the response status for a post or delete does not confirm the database actually changed. Always pair the request with assert_difference.

Best Practices

  1. Test both success and failure paths — For every create or update action, write one test with valid params and another with invalid params that triggers validation errors.
  2. Use fixtures for test data — Fixtures load once per test suite and are wrapped in a transaction, making them faster than factories for controller tests.
  3. Keep controller tests focused on HTTP behavior — Assert status codes, redirects, and response bodies. Push business logic assertions into model tests.

Summary

  • Controller tests in Rails inherit from ActionDispatch::IntegrationTest and exercise the full request cycle.
  • Use get, post, patch, and delete helpers with route URL helpers to simulate requests.
  • Verify outcomes with assert_response, assert_redirected_to, and assert_difference.
  • Always reload ActiveRecord objects after updates and wrap mutations in assert_difference blocks.
  • Test both valid and invalid request scenarios for complete coverage.

Code Examples

ruby
require 'test_helper'

class ProductsControllerTest < ActionDispatch::IntegrationTest
  test 'index returns success and lists products' do
    get products_url
    assert_response :success
    assert_match 'Products', response.body
  end

  test 'create adds a product and redirects' do
    assert_difference('Product.count', 1) do
      post products_url, params: {
        product: { name: 'Widget', price: 9.99 }
      }
    end
    assert_redirected_to product_url(Product.last)
  end

  test 'destroy removes product' do
    product = products(:gadget)
    assert_difference('Product.count', -1) do
      delete product_url(product)
    end
    assert_redirected_to products_url
  end
end
✓ Completed