Testing Is a Design Problem
19 March 2026

Most developers I know have a complicated relationship with tests. They know they should write them. They feel guilty when they don't. When they do, the tests are often fragile — they break on refactors, require elaborate setup, and give no real confidence that the system works.
The common conclusion is that they're bad at testing. The real problem is usually the code.
Untestable code is a symptom
When you sit down to write a test and the setup takes forty lines, or you find yourself mocking half a dozen dependencies, that's not a testing problem. It's the code telling you something. The same property that makes code hard to test — tight coupling, hidden dependencies, functions that do too many things — also makes it hard to reason about, hard to change, and hard to debug in production.
Testability and good design are the same quality expressed differently. A function that's easy to test has a clear contract: given these inputs, produce this output, with no invisible side effects. That's also what makes it reusable, composable, and safe to change.
What I actually look for
The question I ask when I'm building is: can I call this in isolation? Not can I mock everything around it — can I call it with real inputs and verify the output without staging the entire application?
If the answer is no, I look at what's in the way. Usually it's one of a few things: the function reaches directly into a database or external service, it reads from global state, or it mixes orchestration with logic. These aren't testing problems. They're design problems. Separating the logic from the infrastructure — even slightly — usually makes both better.
The mock trap
There's a version of testing that gives you high coverage and low confidence. You mock the database, the HTTP client, the file system. Your tests pass in milliseconds. Then something breaks in production and the tests don't catch it, because they were never testing the real thing.
This is part of why I moved away from Mongoose toward the native MongoDB driver. Mongoose adds enough abstraction that you feel compelled to mock it. With the native driver and typed query functions, the code is direct enough that hitting a real test database — even in CI — is simpler than constructing a convincing mock. The tests are slower. They're also actually useful.
Where tests earn their place
I don't test everything. For solo projects especially, the cost of a comprehensive test suite can exceed the benefit. What I do test:
The seams between systems. API endpoints, database queries, webhook handlers — the places where my code talks to something outside itself. These are where integration breaks and where bugs hide.
Pure logic with meaningful complexity. Pricing calculations, date arithmetic, permission rules. Easy to test, fast to run, and genuinely useful because the logic is the point.
Things I've already broken. When a bug reaches production, I write a test that reproduces it before I fix it. The test is cheap to write at that point, and it means the bug stays fixed.
Testing as feedback
The most useful thing about tests isn't the safety net — it's the feedback loop. When a test is difficult to write, it's usually because the code deserves to be simpler. When a test requires complicated setup, the function is probably doing too much.
If you find yourself avoiding tests because they're painful to write, start there. Not with a testing framework or a coverage target — with the question: why is this hard to call in isolation? The answer is worth acting on regardless of whether you end up writing the test.