Ruby & Rails

The tests worth writing

Coverage measures which lines ran, not whether anything is verified. What actually catches regressions is narrower and cheaper than a coverage target.

27 May 2026 2 min read Mohammad Aaquib Jawed

A suite can hit high coverage and catch nothing, because coverage records which lines executed, not whether any assertion would fail if they misbehaved.

What catches regressions is a smaller, more specific set of tests than most coverage targets produce.

Test the boundaries you promise

The highest-value tests sit at the edges of your system: an HTTP request in, a response out. They exercise routing, controllers, models and views together, and they break when any of it breaks.

They are also the most stable, because they assert on behaviour a user could observe. Refactor freely underneath and they keep passing — which is exactly what you want from a test, and exactly what heavily mocked unit tests fail to deliver.

Test the logic that is genuinely hard

Anything with branching, arithmetic or ordering deserves direct tests: a pricing rule, a date-boundary calculation, an eligibility check, a parser.

These are cheap to write, run instantly, and are where bugs actually live. A test that a model responds to an attribute is not one of them.

Test the bugs you have already had

This is the highest-yield habit available and almost nobody does it consistently.

Every production bug becomes a test before it is fixed. The test fails, the fix makes it pass, and that specific failure can never silently return. Over a year this accumulates into a suite shaped by your actual failure modes rather than someone's idea of what should be tested.

A regression suite built from real incidents is worth more than one built from a coverage target, because reality chose the cases.

What to stop testing

Framework behaviour. A test that validates :email, presence: true rejects a blank email is testing Rails, not your code.

Deep mocks. A test that asserts a method called another method with particular arguments passes when the code is broken and fails when it is refactored — the exact inverse of useful.

Exhaustive permutations of trivial logic. Three representative cases plus the edges beat forty combinations that all exercise the same branch.

Speed is correctness

A suite that takes twenty minutes stops being run. People push without it, or run a subset, and the value collapses regardless of what it covers.

The usual culprits are database setup per test, real HTTP calls, and sleeps. Fix those and a large suite runs in seconds. A fast suite that tests less catches more, because it actually gets run.

All writing Reply by email