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: {...}, thenassert_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
endRemember
- 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.