In short
Test the things that would be expensive to get wrong and are hard to verify by looking: business logic with real consequences, edge cases, and the paths that touch money, permissions, or data loss. Coverage percentage is a poor target because it counts lines executed rather than behaviour verified — a suite can reach 90% while asserting almost nothing that matters.
The question that decides it
For any piece of code: if this broke silently, how would I find out, and what would it cost?
High cost and no obvious signal means test it. Low cost and immediately visible means probably not.
That single question orders a test suite better than any coverage target, because it optimises for the thing tests exist to provide — confidence — rather than for a number.
What is usually worth testing
Business logic with consequences. Pricing, discounts, tax, permissions, eligibility. Wrong answers here cost money or trust, and they can be wrong for months without anyone noticing.
Edge cases you cannot hold in your head. Empty collections, single items, boundaries, maximums, zero, negative numbers, unusual characters. This is where defects actually live.
Anything with a known past failure. A bug that occurred once can occur again. A test written at fix time is the cheapest test you will ever write, and it has already proved its value.
Data transformations. Parsing, formatting, imports, exports. Easy to get subtly wrong, hard to spot by looking.
Critical paths end to end. Signup, checkout, the main flow. One test proving a user can complete the journey is worth many unit tests of its pieces.
Security boundaries. That a user cannot read another's data, that an unauthenticated request is rejected, that a role cannot escalate. These fail silently and catastrophically.
What is usually not worth testing
Framework behaviour. The framework's authors tested that a route resolves.
Trivial getters and setters. Nothing to get wrong, nothing to protect.
Exact markup. Tests asserting specific HTML break on every design change while catching nothing. Assert behaviour and content, not structure.
Implementation detail. A test asserting that a function calls another function fails when you refactor correctly. That is a test that punishes improvement.
Configuration. That an environment variable is read is better verified by the application starting.
A useful heuristic: if the test fails only when the code changes but not when the behaviour changes, it is a liability.
Why coverage percentage misleads
Coverage counts which lines executed during the run. It does not count whether anything was verified.
You can reach high coverage by calling every function and asserting almost nothing. The number looks reassuring and the suite catches nothing — which is worse than no suite, because it produces false confidence.
Coverage is useful in one direction: finding what is not tested at all. A critical module at 5% is worth investigating. A team at 85% has learned little about whether the right things are verified.
If you must set a target, set it on the modules that matter rather than the codebase as a whole.
Choosing the level
Unit tests are fast, precise, and cheap. Right for logic with clear inputs and outputs — calculations, transformations, rules.
Integration tests verify that pieces work together, including the database. Slower, and they catch a class of bug unit tests structurally cannot: wrong queries, broken constraints, incorrect transactions.
End-to-end tests drive the real application. Slowest and most fragile, and the only ones proving the thing actually works. A handful covering critical journeys is right; a hundred is a maintenance burden that will be disabled within a year.
The usual advice is many unit, fewer integration, few end-to-end. That holds, with a caveat: for applications that are mostly moving data between a database and a screen, integration tests often deliver more confidence per unit of effort than a large number of unit tests over thin logic.
Writing tests worth keeping
Test behaviour, not implementation. "Given an expired discount code, the order total is unchanged" survives refactoring. "Calls validateDiscount once" does not.
One reason to fail per test. A test asserting eight things tells you something broke, not what.
Make failure messages informative. You will read them under pressure.
Keep them independent. Tests that depend on order, or on state left by earlier tests, become impossible to debug.
Use realistic data. foo and bar pass; real names with apostrophes, long strings, and unusual characters find bugs.
Write the test that would have caught the bug — before the fix, so you watch it fail for the right reason. A test that passes before the fix is testing something else.
The suite nobody trusts
The worst outcome is not too few tests. It is a suite people ignore.
That happens when tests fail intermittently, so red stops meaning anything. When the suite takes long enough that people skip it. When failures are usually the test's fault rather than the code's.
All three are worth treating as urgent, because a suite people route around provides nothing while still costing maintenance. Delete or fix flaky tests immediately — one unreliable test degrades trust in every other.
Where to start on an untested codebase
Do not aim for coverage. Take the three things that would be most expensive to break — usually payment, permissions, and the main user journey — and write tests for those. Then add a test each time you fix a bug.
Within a few months you have a suite covering exactly the things that actually break, which is the suite you wanted.
If you have inherited a codebase and are not sure what is safe to change, book a call.
Common questions
What should you write tests for?
Anything that would be expensive to get wrong and hard to notice: business logic with real consequences like pricing and permissions, edge cases you cannot hold in your head, data transformations, security boundaries, and the critical user journeys. Also anything that has broken before — a test written at fix time is the cheapest you will write.
Is test coverage percentage a good target?
No, because coverage counts lines executed rather than behaviour verified. A suite can reach 90% while asserting almost nothing that matters, which is worse than no suite because it creates false confidence. Coverage is useful only for spotting what is untested at all, such as a critical module sitting at 5%.
Should I write unit tests or integration tests?
Both, weighted toward the level that gives you confidence per unit of effort. Unit tests suit logic with clear inputs and outputs. For applications mostly moving data between a database and a screen, integration tests often deliver more confidence, because they catch wrong queries and broken constraints that unit tests structurally cannot.
What makes a test not worth keeping?
If it fails when the code changes but not when the behaviour changes, it is a liability. Tests asserting exact markup, or that one function calls another, break during correct refactoring while catching nothing — they punish improvement. Test behaviour and outcomes instead.
How do you fix a test suite nobody trusts?
Treat flaky tests as urgent and delete or fix them immediately, because one unreliable test degrades confidence in every other. Then get the suite fast enough that people do not skip it. A suite developers route around costs maintenance while providing nothing.
