Ravi Kumar
All articles

61% to 90%: How I Actually Raised Test Coverage on a Solo SaaS

Her Aviation Era's Codecov project floor is 90% today. It didn't start there — it started at a risk-based 70%, scoped deliberately, then got raised once the number was real. The scoping decision mattered more than the number.

Scope before percentage

Chasing "90% of the codebase" would have meant writing shallow tests for ~250 UI components and pages just to move a number — the kind of coverage that passes CI and catches nothing. Instead, coverage targets lib/**, app/api/**, and app/actions/** only: money, access control, entitlements, scrapers, booking logic — the surfaces where a silent regression costs a user their payment or their data. Components and pages stay untested until a bug in one ships twice. That's not a gap I'm ignoring; it's a bet that testing effort is worth more concentrated on lib/orders.ts than spread across a hero section.

Two floors, not one

Codecov enforces two different targets on every PR, and they answer different questions. patch: 70% asks "are the lines this PR actually touched tested?" — the number that matters day to day. project: 90%, threshold: 1% is a hard floor on the whole scoped surface: a PR that drags total coverage down more than one point fails CI outright, full stop. The project floor only went in once coverage had actually reached 90% for real — raising the bar before hitting it would have just meant failing CI on unrelated work.

A pre-commit gate that mirrors CI

Finding out you're under the coverage floor after opening a PR is a slow feedback loop. .husky/pre-commit runs vitest --coverage on every commit to a feature branch and fails the commit if the local thresholds (lines/statements ≥ 90%, mirroring codecov.yml) aren't met — skipped on main/staging, which only ever receive merges, never a direct commit. New code in the tested scope ships with a colocated test in the same commit, or the commit doesn't happen.

Vitest and Codecov don't agree, and that's fine

The first time local coverage read 98% but Codecov's PR comment showed 89%, it looked like a bug. It isn't. Vitest counts line coverage — a line that executed at all counts as covered. Codecov additionally requires every branch on that line to have been taken; a && b where b never evaluated is a partial, and partials don't count toward Codecov's headline percentage. A local run around 98% routinely lands 8–10 points lower on Codecov. Knowing the gap in advance meant I set the local Vitest threshold to 90% as a floor to defend, not a number I expected to match Codecov exactly — and verify against the actual PR comment before assuming a target's hit.

What actually changed the number

Not a testing sprint — a rule. Every new lib/, app/api/, or app/actions/ change ships with its own test, colocated next to the file it tests (lib/coupons.tslib/coupons.test.ts), pulling from shared fixtures and a mocked Supabase query builder in test/. Coverage climbed as a side effect of that rule holding for months, not from going back to backfill a number.

More on the release discipline this fits into is in the Her Aviation Era case study.