The Trap of 100% Test Coverage: Don't Be Fooled by the Numbers
1. The Lure of the Green Bar
As a junior developer, I was addicted to the 'green bar' shown by Jest Coverage reports.
Seeing Branch Coverage: 85% bothered me.
"If I make this 100%, our code will be bug-free!"
I stayed up nights writing test codes.
I forced tests onto simple Getter/Setters and even Catch blocks that seemed impossible to reach.
The day I finally hit 100%, I proudly told my team:
"You don't have to worry about bugs anymore!"
Then the next day, the payment logic exploded in production.
2. It's 100%, So Why the Bug?
The cause of the bug was simple.
I failed to consider the "Denominator is 0" case in division.
/* Real Code (Coverage 100%) */
function calculateDiscount(price, rate) {
return price / rate;
}
/* Test Code */
test('Calculate Discount', () => {
expect(calculateDiscount(1000, 2)).toBe(500);
});
This code has 100% coverage.
Every line inside calculateDiscount was executed.
But it failed to catch the business logic error where the program crashes or returns Infinity when rate is 0.
Coverage only tells you "Did the code run?", not "Does the code work correctly?".
Reading every single word of a book (100% coverage) doesn't mean you understood the content.
3. The Sin: Meaningless Tests
Wait to see the worst test I wrote just to pump up the numbers.
/* Meaningless Test */
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
test('Get User Name', () => {
const user = new User('Ratia');
// Calling getName() increases coverage
expect(user.getName()).toBe('Ratia');
});
This test checks if getName simply returns this.name.
What are the odds of this breaking? Near zero.
But as these tests accumulate, they just increase test execution time and become baggage to maintain whenever refactoring.
Tests solely for filling numbers are technical debt that only increases maintenance costs.
4. Is Coverage Useless Then?
No. Coverage is excellent as an indicator of "What we haven't tested".
- Spotting Low Coverage: "Huh? There are zero tests for Order Cancellation logic?" -> Identifying risks.
- Finding Dead Code: "This function is never executed in any test suite? Is it unused?" -> Review for deletion.
Coverage should be a Tool, not a Target.
Remember Goodhart's Law:
"When a measure becomes a target, it ceases to be a good measure."
5. What is the Sweet Spot?
Even big tech companies like Google or Facebook don't aim for 100%.
Generally, 60% ~ 80% is considered healthy.
- Core Business Logic (Payment, Auth): Over 90%, covering edge cases thoroughly.
- Simple Utility/UI: Around 50%.
- Config/Boilerplate: 0% is fine.
What matters is not "How high is the percentage" but "What did you test".
Testing failure cases (exceptions) with lower coverage is far more valuable than hitting 100% by only testing the Happy Path.
6. Testing Types and Costs (Testing Pyramid)
Obsessing over numbers leads to hoarding "Cheap Tests".
Tests have ranks too.
6.1. Unit Test
- Target: Single function or class.
- Cost: Very Cheap. Blazing fast.
- Feature: Good for logic verification, but doesn't tell if the whole system works.
6.2. Integration Test
- Target: API + DB, Frontend Component + Store.
- Cost: Medium. Needs DB setup.
- Feature: Best ROI. Even Kent Beck says "Write mostly integration tests".
6.3. E2E Test (End-to-End)
- Target: Spinning up real browser and clicking (Cypress, Playwright).
- Cost: Very Expensive. Slow. Flaky.
- Feature: Closest to User Experience, but maintenance is hell. Use only for critical paths (Payment).
Recommended Ratio: Unit (50%) : Integration (40%) : E2E (10%)
7. A Taste of TDD (Test Driven Development)
If coverage feels like homework, flip the order.
Don't write code then add tests. Write tests first, then write code to match.
- Red: Write a failing test. (
expect(add(1, 2)).toBe(3))
- Green: Write minimum code just to pass the test.
- Refactor: Clean up the code.
This way, "Code without tests cannot exist", so 100% coverage becomes a Natural Consequence, not a goal.
8. Recommended Tools
If you are using JavaScript/TypeScript, these are the standard tools.
- Jest: The All-in-One King. Includes Test Runner, Assertion Library, and Coverage (Istanbul).
- Vitest: The rising star. Built on Vite, it's blazing fast. Highly recommended for new projects.
- Cypress: The standard for E2E testing. Easy to use but slight learning curve.
- Playwright: Microsoft's E2E tool. Faster and more stable than Cypress. Supports multiple tabs and languages.
9. Testing Terminology Cheat Sheet
Confused by testing jargon? Here is a quick guide.
- Mock: A fake object that simulates the behavior of a real object. Used when you want to control the output (e.g., "return 200 OK").
- Stub: A dummy implementation that does nothing or returns hardcoded values. Simpler than a Mock.
- Spy: Wrapper around a function to record arguments and return values. Used to verify "Was this function called?".
- Black Box Testing: Testing without knowing the internal code structure. (Input -> Output).
- White Box Testing: Testing with full knowledge of the internal logic. (Coverage measures this).
- Regression Testing: Re-running old tests to make sure new code didn't break existing features.
- Smoke Test: A quick sanity check to see if the server even turns on.
9. FAQ
Q. Should I test Private Methods?
A. No. Private methods are Implementation Details. They should be tested implicitly via Public methods. If you export them just to test, you break encapsulation and make refactoring hard.
Q. My test code is longer than production code. Is this right?
A. Yes, it's normal. Usually test code is 2-3x longer. Be proud, it means you are defending against various edge cases.
Q. When should I Mock?
A. Only for things You cannot control (External APIs, Time, Randomness). For DB or File System, it's easy to spin up Docker containers these days, so use the real thing if possible.
9. Conclusion: Don't Be a Slave to Numbers
Are you anxious because your coverage is 40%?
It's fine. If critical logic is protected, 40% is great.
Are you relaxed because it's 99%?
A bug hiding behind those numbers might be laughing at you.
The goal of testing is not to score points, but to "Deploy with Confidence tomorrow".
Instead of the green bar, use "Confidence" as your metric.
10. Recommended Reading
If you want to dive deeper into testing philosophy:
- "Test Driven Development: By Example" by Kent Beck. The bible of TDD.
- "Working Effectively with Legacy Code" by Michael Feathers. Essential for dealing with untestable code.
- "Unit Testing Principles, Practices, and Patterns" by Vladimir Khorikov. A modern take on pragmatic testing.