| name | rails-testing-patterns |
| description | Rails/Minitest test-type selection (integration over system tests), XSS dual-verification, assert_select scoping, AJAX contract testing, and object-staleness (.reload) pitfalls. Minitest/ActiveSupport::TestCase examples — adapt for RSpec. |
| license | MIT |
| metadata | {"version":"2.1.0","hermes":{"tags":["Rails","Testing","Patterns","Quality"],"related_skills":["rails-cache-performance","test-plan-methodology"]}} |
Rails Testing Pattern Selection
Test Type Decision
| Question | Test Type |
|---|
| Requires real browser (modals, JS-heavy UI)? | System test (last resort — markedly slower: browser boot + JS) |
| Controller logic, routing, template rendering, security? | Integration test |
| Frontend controller / DOM manipulation? | JavaScript test (Jest/Mocha) |
| Business logic, validations, associations? | Model test |
Default to integration tests. System tests only for genuine browser interaction.
XSS Dual-Verification
Simple absence checks fail when content is HTML-escaped. Always verify both sides:
test 'escapes user input in HTML' do
item = create(:item, name: '<img onerror="alert(1)">')
get item_path(item)
assert_includes response.body, '<img'
assert_not_includes response.body, '<img onerror="alert'
end
assert_select Patterns
Use minimum: for variable collections, count: only for fixed structures. Always scope utility classes to a container:
assert_select '.product-card', minimum: 1
assert_select 'form.product-form', count: 1
assert_select '#product-details' do
assert_select '.text-center', minimum: 1
end
Contract Testing for AJAX
Test the AJAX endpoint directly — AJAX content is absent from the initial page render. Assert contract shape (keys, types), not specific values:
test 'returns expected JSON structure' do
get api_products_path, as: :json
json = JSON.parse(response.body)
assert json.key?('products')
assert_not_empty json['products']
assert json['products'].first.key?('id')
end
(assert_select lives in the rails-controller-testing gem since Rails 5 — add it if the helper is undefined.)
Staleness Pitfalls
Three common variants of the same root cause: the Ruby object doesn't reflect the current DB state. Fix: call .reload before assertions or path helpers.
| Scenario | What goes stale | Fix |
|---|
Create child via parent_id:, then read parent.children | Association cache | parent.reload |
Update a FriendlyId model, then use product_path(product) | Slug attribute | product.reload |
| Factory creates its own association, ignoring your override | Implicit association | Pass association explicitly and verify with .reload |
parent = create(:parent)
create(:child, parent_id: parent.id)
parent.reload
Naming Convention
Prefix integration test classes with the type suffix to avoid collisions across directories: FooIntegrationTest < ActionDispatch::IntegrationTest, not FooTest.