i said i quit testing. then i shipped seven things.
a year ago i declared testing a waste of time for solo devs. then a silent bug ate 79% of my data. an honest retraction.
about a year ago i wrote a post declaring, with the serene confidence of a man who has never once been burned, that testing was a waste of time for a solo developer. write the feature, click around, push to prod, let the users find the bugs. i called it — and i am quoting myself here — “testing enlightenment stage three.”
i’d like to retract about sixty percent of that.
not all of it. that’s the annoying thing about past-me: he wasn’t entirely wrong, he was wrong in a specific, expensive, instructive way. so this is the follow-up he owes you — what shipping seven more things taught me about which forty percent to keep.
the forty percent i’d still defend
the original post had a real point buried under the bravado. i’d spent four days building a beautiful test suite for a feature — a fighter-promotion system, belts and eligibility and points — and two weeks later one conversation with one potential user deleted the entire feature. four days of tests for code that never met a human. that waste was real, and the lesson survives intact: do not write exhaustive tests for speculative product surface whose requirements will change next sprint. before product-market fit, most of your code is a question, not an answer, and you don’t write regression tests for a question.
that part i still believe. chasing test coverage as a goal is vanity. i’ll die on that hill.
the sixty percent that nearly cost me everything
here’s where past-me walks calmly into the sea.
“test in production,” he said. “let the users find the bugs.” so let me tell you about the bug the users could never have found. i built a voting toy , it kept an elo rating, and under concurrent votes it silently lost seventy-nine percent of every rating change. seventy-nine percent. for weeks. and production was delighted — every vote returned a 200, every row got written, every count added up. “test in production” didn’t catch it because production had no idea anything was wrong. the corruption lived in the gap between two requests, and no user, no dashboard, no error log was ever going to see it.
one thing caught it: a test that fired fifty concurrent votes at a real database and checked whether the number at the end was the number it should be. i had that tool. i’d written a whole series about it. and past-me’s philosophy would have shipped that bug forever, because his philosophy only defends against failures loud enough to file a complaint.
that’s the flaw, stated plainly: “test in production” is a strategy for visible failures. it is worthless against silent ones. and the silent ones are the ones that actually get you.
the rule that survived contact with reality
so somewhere across the seven projects, “test everything” and “test nothing” both lost, and what’s left is a single question i now ask of every path:
if this is wrong, will i find out?
if being wrong is loud — a 500, a crash, a blank page — fine, production will tell me. test it lightly, move on. past-me handles that case just fine. but if being wrong is silent (a lost update, a mis-signed webhook), or irreversible (deleted data, a double charge), or invisible until it’s a lawsuit (an auth check that doesn’t check), that path gets a test before it ships. no exceptions, no “i’ll get to it.”
turns out i’d been quietly obeying this rule for a year without writing it down:
- the go services get a real postgres in a container , not mocks, because a mock can’t catch a broken trigger or a foreign key i forgot — and those fail silently.
- the rust tools get fast fake-network tests by default and real-api tests behind a flag, so a change that breaks the actual contract can’t slip through a green ci.
- the one project i run that actually takes money has a regression test that captures the real idempotency key off the wire — because a refactor to a random id would silently re-enable double-billing, and “silently” plus “billing” is the exact intersection this whole rule exists for.
- and the voting toy, after it humbled me, got seeded with a fake gone-viral database and hammered with concurrent load until the invariant held. that’s not coverage. that’s “would this ruin me?” answered on purpose, instead of in production, at the worst possible time.
none of that is “all the tests.” it’s aim.
the metric, corrected
past-me closed with the metrics that actually matter for a solo dev: time to ship, customers, revenue — not test coverage percentage. and he was right. i just add one line to his list now:
is there a test on every path where being silently wrong costs me a customer or a euro?
coverage percentage measures how much of your code you happened to touch. that question measures whether you’ll survive the thing you didn’t see coming. one is vanity. the other is the entire job.
so, seven things later
i didn’t quit testing. i quit pretending testing was one decision. it isn’t “on” or “off,” it’s a targeting problem: point your limited, precious testing effort at the paths where failure is silent, expensive, or irreversible, and let production handle the loud stuff it’s actually good at catching. past-me thought the choice was tests versus shipping. the real choice was tests versus finding out at the worst possible moment.
- test the thing that would ruin you. skip the thing that’ll change. coverage percentage measures neither, which is exactly why it’s the wrong number to chase.
- “test in production” is a real strategy — for loud failures only. for the silent ones it’s just hope wearing a lab coat.
- the goal was never zero tests, and it was never a hundred percent. it was aim. past-me had the full courage of his convictions and none of the accuracy.
patricia, my old qa lead, if you ever read this: you were more right than i gave you credit for. not all the way — i still think you’d have written tests for the fighter-promotion feature that got deleted, and i’d still have deleted them. but the seventy-nine percent? that one’s yours. i should’ve listened sooner.