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/modelsstructure intest/models:app/models/card/closeable.rb↔test/models/card/closeable_test.rb; shared concerns undertest/models/concerns/. - UUID PKs break fixture ordering: generate deterministic, label-derived UUIDv7s in fixtures so
.first/.lastare 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_tofor 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)andassert_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) andas: :json(status, Location header, body). - Turbo/broadcast assertions by layer:
assert_turbo_stream_broadcastsin model tests,assert_turbo_stream action:, target:in controller tests,assert_no_turbo_stream_broadcastsfor 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(andCurrent.sessionwhen behavior depends on the actor) in setup; integration/system tests setdefault_url_options[:script_name]; provide anuntenanted { }helper for auth routes. ClearCurrentin 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.