What's Worth Testing (Straight Talk)
If Phase 1 sold you on the net and Phase 2 showed you it's not magic, there's a real risk you swing too far the other way and try to test everything. That feels responsible. It isn't. Tests cost time to write and, more importantly, time to maintain forever. The real skill isn't "write lots of tests"; it's knowing which tests pay for themselves and which ones quietly drain you - the cost/benefit conversation a good senior would actually have with you.
The cheat-card: test it or skip it?
When you're staring at a piece of code wondering "should this have a test?", run it through this:
| The code is... | Worth a test? | Why |
|---|---|---|
| Real logic (calculations, rules, parsing) | Yes | This is where bugs live and where they cost real money. |
| A bug you just fixed | Yes | Lock it shut so it can never come back. |
| A tricky edge case (empty, zero, negative, max) | Yes | The exact spots everyone forgets - so they break in prod. |
| A trivial getter / one-line passthrough | Usually no | Nothing to get wrong; the test just adds maintenance. |
| Code that only forwards to a library you trust | Usually no | You'd be testing the library, not your code. |
| Throwaway / experimental code | No | It'll be gone next week. |
The pattern: test where mistakes are both likely and expensive. Skip where a mistake is nearly impossible or harmless.
Test the logic that actually matters
What it actually is. "Logic that matters" is any code where the answer can be wrong in a way that hurts - money, data, access, anything a user would feel. A discount calculation. A permission check. A function that decides whether an order can ship. These are the load-bearing walls of your program.
Why people get the priority wrong. Beginners often test the easy stuff (a getter that returns a name) because it's easy, and skip the scary stuff (the tax calculation with five branches) because it's hard. That's exactly backwards - the easy code is easy because there's nothing to get wrong. Put your testing effort where the thinking is, not where the typing is fast.
Why this saves you later. A wrong name in a getter is an annoyance. A wrong number in a billing calculation is a refund, an angry email, maybe a finance audit. Your tests should stand guard over the code that can ruin your week, not the code that can't.
Test the bug you just fixed (every time)
This is the single highest-value habit in this whole guide, so it gets its own section.
What it does in real life. When you find and fix a bug, you write a test that fails on the broken code and passes on the fix. That test now stands guard over that exact mistake forever.
📝 Terminology. This is called a regression test - a test written specifically to make sure a bug you already fixed can never quietly come back (a "regression," from Phase 1). It's the most cost-effective test you can write, because you've already done the hard part: you know exactly what was wrong.
$ npm test
✓ does not double-charge when the cart is empty ← the bug we fixed in March
What just happened: months after fixing an empty-cart double-charge bug, this little test is still on duty. The day a teammate refactors the cart and accidentally reintroduces the exact same bug, this test goes red and catches it before a customer gets charged twice. You paid for that protection once and it keeps working for free.
💡 Key point. Never fix a bug without leaving a test behind. A bug fix without a regression test is a bug you've agreed to fix again later.
Test the tricky edges, not the happy path twice
The "happy path" - normal inputs, everything fine - is worth one test. But bugs cluster at the edges: the inputs people forget exist.
// The happy path is one test. The edges are where the bugs hide:
;
;
;
What just happened: one test covers the normal case, and two more cover the cases that actually bite - zero people (a classic divide-by-zero crash) and money that doesn't divide cleanly (a rounding bug waiting to happen). Ten more tests with different happy-path numbers would add maintenance without finding new bugs. The edges are where the value is: empty, zero, negative, the maximum, the unexpected type.
Two ways tests turn into a liability
Tests aren't free, and badly-chosen ones actively hurt. Two failure modes to recognize:
⚠️ Tests that test nothing. A test that can't fail when the behavior breaks is worse than no test - it gives you false confidence (a net with a hole in it). The classic is asserting something that's true no matter what:
// This test will pass even if priceFor is completely broken.
;
What just happened: this looks like a test and shows up green, but priceFor could return 80, 0, or
-9999 and it would still pass - they're all numbers. It guards nothing. (Compare it to the Phase 2 test
that asserted toBe(90), which actually pins down the behavior.) Always ask: what wrong behavior would make
this test go red? If the answer is "nothing," delete it.
⚠️ Brittle tests. A brittle test breaks every time you touch unrelated code, even when nothing's actually wrong. Usually it's checking how the code did something instead of what it produced - exact log wording, internal call order, private details. The cost shows up later: every refactor turns the suite red for no real reason, you start ignoring failures, and a net you ignore isn't a net. Test the result a user cares about, not the private path the code took to get there.
📝 Terminology. Code coverage is the percentage of your code lines that get run while the tests execute. It's a useful hint about what's untested, but it's a terrible goal. 100% coverage can be all the "tests that test nothing" above - every line runs, nothing is actually checked. High coverage of meaningless tests is a green dashboard sitting on top of an unprotected codebase.
⚠️ Don't chase 100%. Coverage measures how much code ran, not how much is protected. Chasing the last few percent usually means writing pointless or brittle tests for trivial code - paying maintenance forever to make a number go up. Aim to cover the logic that matters, the bugs you've fixed, and the edges. That's the coverage that actually catches things.
Recap
- Test where mistakes are likely and expensive: real logic, the bug you just fixed, the tricky edges.
- Skip the trivial: getters, one-line passthroughs, code that just forwards to a trusted library.
- Always leave a regression test behind a bug fix - it's the cheapest, highest-value test you'll write.
- Edges over repetition: one happy-path test, then the zeros, empties, negatives, and odd inputs.
- Beware tests that test nothing (can't fail) and brittle tests (fail for no real reason) - both erode trust in the net.
- Coverage is a hint, not a goal. 100% of meaningless tests protects nothing.
That's the full mental model: why tests matter (the net), what a test is (call the code, check the answer), and what's worth testing (the logic, the bugs, the edges). You now understand testing well enough that writing one will feel obvious instead of mysterious.
When you're ready to actually sit down and write your first one start to finish, go to Your First Unit Test. And when you want the map of the different kinds of tests - unit, integration, end-to-end - and when to reach for each, read Unit, Integration, and E2E.
← Guide overview · Phase 2: What a Test Actually Is
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What kind of code is most worth testing?
2. Why write a regression test after fixing a bug?
3. Why is code coverage a hint rather than a goal?