| name | rails-testing |
| description | TDD and testing patterns for Rails applications using Minitest and fixtures. Use when writing tests, setting up test data, or debugging test failures. |
| trigger | test, minitest, fixture, assert, testing, spec, coverage, tdd |
Rails Testing Guide
Test-Driven Development
Write tests BEFORE implementation.
TDD Cycle
1. RED → Write a failing test
2. GREEN → Write minimal code to pass
3. REFACTOR → Clean up, tests stay green
TDD Workflow
test "user can close a card" do
card = cards(:open)
assert_not card.closed?
card.close
assert card.closed?
assert_equal users(:one), card.closure.closed_by
end
What to Test First
| When adding... | Write first... |
|---|
| Model method | Unit test for the method |
| Controller action | Integration test for endpoint |
| User feature | System test with Capybara |
| Bug fix | Test that reproduces the bug |
Test Naming
Describe behavior, not implementation:
test "closing a card creates a closure record"
test "user cannot close cards they don't own"
test "close method calls create_closure!"
test "Closeable concern is included"
Philosophy
"Write tests for behavior, not implementation. Use fixtures, not factories."
Core Principles:
- TDD: Tests first, then implementation
- Minitest over RSpec
- Fixtures over FactoryBot
- Test public interface
- One assertion per test (when reasonable)
- Fast tests enable TDD
Test Types
| Type | Location | Purpose |
|---|
| Unit | test/models/ | Model behavior |
| Controller | test/controllers/ | Request/response |
| Integration | test/integration/ | Multi-step flows |
| System | test/system/ | Browser tests |
| Mailer | test/mailers/ | Email content |
| Job | test/jobs/ | Background jobs |
Fixtures
Basic Fixtures
admin:
id: 01961a2a-c0de-7000-8000-000000000001
email: admin@example.com
name: Admin User
regular:
id: 01961a2a-c0de-7000-8000-000000000001
email: user@example.com
name: Regular User
Associations
main:
id: 01961a2a-c0de-7000-8000-000000000001
name: Main Board
owner: admin
open_card:
id: 01961a2a-c0de-7000-8000-000000000001
board: main
author: admin
title: Open Card
closed_card:
id: 01961a2a-c0de-7000-8000-000000000001
board: main
author: admin
title: Closed Card
ERB in Fixtures
<% 10.times do |i| %>
card_<%= i %>:
id: 01961a2a-c0de-7000-8000-000000000001
board: main
author: admin
title: Card <%= i %>
created_at: <%= i.days.ago %>
<% end %>
Polymorphic Associations
card_closure:
id: 01961a2a-c0de-7000-8000-000000000001
closeable: closed_card (Card)
closed_by: admin
Model Tests
class CardTest < ActiveSupport::TestCase
setup do
@card = cards(:open_card)
@user = users(:admin)
Current.user = @user
end
teardown do
Current.reset
end
test "belongs to board" do
assert_equal boards(:main), @card.board
end
test "can be closed" do
assert_not @card.closed?
@card.close
assert @card.closed?
assert_equal @user, @card.closure.closed_by
end
test "cannot close twice" do
@card.close
assert_raises(ActiveRecord::RecordInvalid) { @card.close }
end
test "open scope excludes closed cards" do
closed = cards(:closed_card)
assert_includes Card.open, @card
assert_not_includes Card.open, closed
end
end
Controller Tests
class CardsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:admin)
@card = cards(:open_card)
sign_in @user
end
test "index shows cards" do
get cards_path
assert_response :success
assert_select "article.card", minimum: 1
end
test "show displays card" do
get card_path(@card)
assert_response :success
assert_select "h1", @card.title
end
test "create redirects to card" do
assert_difference "Card.count" do
post cards_path, params: {
card: { title: "New Card", board_id: boards(:main).id }
}
end
assert_redirected_to card_path(Card.last)
end
test "unauthorized user cannot create" do
sign_out
post cards_path, params: { card: { title: "Test" } }
assert_redirected_to new_session_path
end
end
System Tests
class CardsTest < ApplicationSystemTestCase
setup do
@user = users(:admin)
sign_in @user
end
test "creating a card" do
visit board_path(boards(:main))
click_on "New Card"
fill_in "Title", with: "System Test Card"
fill_in "Description", with: "Created via system test"
click_on "Create Card"
assert_text "System Test Card"
assert_text "Card was successfully created"
end
test "closing a card" do
card = cards(:open_card)
visit card_path(card)
click_on "Close Card"
assert_text "Card closed"
assert_selector ".badge", text: "Closed"
end
test "inline editing with Turbo" do
card = cards(:open_card)
visit card_path(card)
within turbo_frame("card_#{card.id}") do
click_on "Edit"
fill_in "Title", with: "Updated Title"
click_on "Save"
end
assert_text "Updated Title"
assert_no_selector ".loading"
end
end
Test Helpers
class ActiveSupport::TestCase
parallelize(workers: :number_of_processors)
fixtures :all
def sign_in(user)
post session_path, params: { email: user.email }
follow_magic_link_for(user)
end
def sign_out
delete session_path
end
private
def follow_magic_link_for(user)
token = user.generate_token_for(:magic_link)
get verify_session_path(token: token)
end
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
def sign_in(user)
visit new_session_path
fill_in "Email", with: user.email
click_on "Send Magic Link"
token = user.generate_token_for(:magic_link)
visit verify_session_path(token: token)
end
end
Assertions
assert_equal expected, actual
assert_not_equal unexpected, actual
assert predicate
assert_not predicate
assert_nil value
assert_not_nil value
assert_includes collection, item
assert_empty collection
assert_difference "Card.count", 1 do
end
assert_no_difference "Card.count" do
end
assert_raises(ActiveRecord::RecordInvalid) { invalid_action }
assert_response :success
assert_redirected_to card_path(@card)
assert_select "h1", "Expected Title"
assert_select "article.card", count: 5
Running Tests
rails test
rails test test/models/card_test.rb
rails test test/models/card_test.rb:42
rails test -n /close/
rails test --verbose
rails test --fail-fast
rails test:system
COVERAGE=true rails test
Best Practices
DO
- Test public behavior, not private methods
- Use descriptive test names
- Keep tests independent
- Use fixtures for test data
- Run tests frequently
DON'T
- Don't test Rails itself
- Don't test private methods
- Don't share state between tests
- Don't use FactoryBot
- Don't mock everything