Testing Rails apps

Ruby on Rails · Practical · 15 min

Rails' built-in testing stack lets you change code confidently, and every generator creates test stubs for you.

Rails uses Minitest with Rails-specific helpers. Run everything with bin/rails test, a file with bin/rails test test/models/article_test.rb, or one test by line: ...article_test.rb:12. Tests run in parallel and in random order, each inside a transaction that's rolled back.

Kinds of tests:

  • Model tests (ActiveSupport::TestCase): validations, scopes and business methods.
  • Integration tests (ActionDispatch::IntegrationTest): real HTTP requests through routes, middleware and controllers: get articles_url, post articles_url, params: {...}, then assert_response :success, assert_redirected_to, assert_select "h1", "Articles".
  • System tests (ApplicationSystemTestCase): drive a real browser through Capybara (visit, fill_in, click_on, assert_text). Slowest, so keep them for critical journeys.
  • Job and mailer tests: assert_enqueued_with(job: ...), perform_enqueued_jobs, assert_emails 1.

Fixtures in test/fixtures/*.yml load named sample records before the suite: articles(:published) returns one. They are fast and referenced by name, and associations can point to other fixtures by label. Many teams use factory_bot instead.

Useful helpers: assert_difference("Article.count", 1) { ... }, assert_changes, travel_to(Time.zone.local(2025, 1, 1)), and sign_in_as(user) helpers for authenticated tests.

RSpec (rspec-rails) is a popular alternative with the same concepts and a different syntax.

Example

# test/fixtures/articles.yml
# published:
#   title: Hello
#   slug: hello
#   published: true
#   author: ada          # references users(:ada)

# test/integration/articles_flow_test.rb
require "test_helper"

class ArticlesFlowTest < ActionDispatch::IntegrationTest
  setup { sign_in_as users(:ada) }     # your test helper that signs in
  test "creates an article" do
    assert_difference("Article.count", 1) do
      post articles_url, params: { article: { title: "New", body: "Text" } }
    end
    assert_redirected_to article_url(Article.last)
    follow_redirect!
    assert_select "h1", "New"
  end
  test "rejects a blank title" do
    post articles_url, params: { article: { title: "" } }
    assert_response :unprocessable_entity
  end
end

Remember

  • bin/rails test runs Minitest in parallel with transactional isolation
  • Integration tests cover routes to views; system tests drive a real browser
  • Fixtures provide named sample records; assert_difference checks side effects

Watch out for

  • Writing mostly slow system tests for logic that model or integration tests could cover faster.

Check yourself

  1. Which test type exercises routing, controllers and views through HTTP without a browser?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: guides.rubyonrails.org/testing.html