What 80% Test Coverage Actually Taught Me
I set a test coverage goal for Aegis2FA. The number didn't matter as much as what writing tests forced me to think about.
I set a rule for myself on Aegis2FA: no calling it "done" until I hit 80% test coverage.
The number was arbitrary. But the process of getting there taught me more about my own code than the coverage report ever showed.
What Actually Happened
Writing tests forced me to use my own APIs. And I found bugs I would have shipped otherwise.
Token reuse bug: I wrote a test that verified a TOTP code, then tried the same code again. It worked both times. That's a security hole. I wouldn't have thought to check that manually.
Race condition in backup code generation: Two concurrent requests could generate overlapping codes. The test failed intermittently, which is how I knew something was wrong. Fixed it with a simple Redis lock.
Password length issue: I never thought about what happens with a 10,000 character password. The test did. Turns out Argon2 gets very slow with long inputs, which is basically a DoS vulnerability. Added a max length check.
The Testing Pyramid Works
I ended up with roughly:
- 60% unit tests (fast, test business logic)
- 30% integration tests (hit the real database)
- 10% E2E tests (full user flows)
Most of my bugs came from integration tests. Unit tests are great for catching regressions, but they don't catch the weird stuff that happens when your code talks to a real database.
The Pattern I Use Now
For auth code specifically, I always test:
- The happy path (valid credentials work)
- Invalid inputs (wrong password, missing fields)
- Edge cases (token expiry, reuse, concurrent requests)
- Rate limits (does the 6th request actually get blocked?)
it('should reject reused TOTP token', async () => {
const token = generateToken();
// First use works
const res1 = await verifyToken(userId, token);
expect(res1.status).toBe(200);
// Second use should fail
const res2 = await verifyToken(userId, token);
expect(res2.status).toBe(401);
});That's the test that caught my token reuse bug. Simple, but I wouldn't have thought to write it if I wasn't aiming for coverage.
What I'd Tell Past Me
Integration tests matter more than unit tests for auth code. Unit tests are fast but they lie to you about how things work together.
Coverage numbers don't equal quality. But they do force you to think about paths through your code you wouldn't otherwise consider.
Write the test before fixing the bug. When I find a bug now, first thing I do is write a failing test. Then I fix it. The test becomes documentation that the bug existed and proof it won't come back.