AI agent skill · by @marckohlbrugge
Rails Testing
Apply Rails testing standards with Minitest, fixtures, and pragmatic coverage boundaries. Use when creating tests, reviewing test quality, or improving flaky and slow Rails test suites.
Archived 2026-10-23 · Source updated 2026-06-09. Check the original repository for the latest version.
SKILL.md instructions
# Rails Testing
Use for test-writing and test-review tasks. Patterns from Campfire and Fizzy test suites.
## Defaults
- Minitest + fixtures. No RSpec, no FactoryBot.
- Test behavior, not implementation details.
- Keep tests deterministic and fast; `parallelize(workers: :number_of_processors)`.
- Tests ship in the same commit/PR as the feature — not before, not later. Security fixes always include a regression test.
- Never add production complexity for testability (no test-induced design damage).
## Coverage Budget (where 37signals actually spends)
- **Heavy:** model tests (domain invariants, concerns) and controller/integration tests (full request cycle, auth, formats).
- **Light:** a few system tests for the critical happy paths (one smoke test can cover signup→use); job tests only for jobs with real logic.
- **None:** view tests, JS/Stimulus unit tests, exhaustive channel tests. UI behavior is covered indirectly by system tests.
- Don't duplicate the same behavior assertion at multiple layers.
## Fixtures
- Express relationships by label, not ID; use ERB for relative timestamps (`created_at: <%= 1.hour.ago %>`) and shared computed values (one bcrypt digest reused).
- Mirror `app/models` structure in `test/models`: `app/models/card/closeable.rb` ↔ `test/models/card/closeable_test.rb`; shared concerns under `test/models/concerns/`.
- UUID PKs break fixture ordering: generate deterministic, label-derived UUIDv7s in fixtures so `.first`/`.last` are stable and runtime records are always newer.
- Build rich-content fixtures with production code (`ActionText::Attachment.from_attachable(user).to_html`), not hand-written markup.
## Good Practices
- Use system/integration tests for user workflows; model tests for domain invariants.
- `travel_to` for time-based logic.
- Mock/stub only at boundaries (external APIs, network, time, `SecureRandom`); use VCR for external HTTP — auto-name cassettes from class+test name, normalize timestamps in matching.
- Test async side effects from model tests with `perform_enqueued_jobs(only: SpecificJob)` and `assert_enqueued_with` — not by unit-testing trivial job classes.
- Correlated count changes in one assertion: `assert_difference({ -> { card.assignees.count } => -1, -> { Event.count } => +1 })`.
- Test both response formats where controllers serve them: `as: :turbo_stream` (assert stream targets) and `as: :json` (status, Location header, body).
- Turbo/broadcast assertions by layer: `assert_turbo_stream_broadcasts` in model tests, `assert_turbo_stream action:, target:` in controller tests, `assert_no_turbo_stream_broadcasts` for negatives.
- Authorization tests assert the negative space: cross-tenant/role access returns 403/404, not just that allowed access works.
- Multi-tenant suites: set `Current.account` (and `Current.session` when behavior depends on the actor) in setup; integration/system tests set `default_url_options[:script_name]`; provide an `untenanted { }` helper for auth routes. Clear `Current` in teardown.
- Test middleware in isolation with `Rack::MockRequest`.
- System tests: `using_session("Kevin")` for multi-user scenarios; wait for cable connection before asserting realtime; auth via a fast session-transfer helper, keeping the full login flow to one smoke test.
- Suites with non-transactional side effects (FTS tables) opt out per-helper: `self.use_transactional_tests = false` + explicit cleanup.
- Reset shared global state per test in parallel suites (thread pools, `ActionCable.server.pubsub`, `Current`).
## Red Flags
- Adding production complexity only for testability.
- Over-mocking internal app code.
- Duplicate tests for the same behavior at multiple layers.
- Slow suites caused by unnecessary setup in each test (that's what fixtures are for).
- Unit tests for one-line job classes or trivial delegations.
- Hand-rolled HTML strings where production renderers/helpers would stay in sync automatically.
- Time-dependent assertions without `travel_to`.