How I use Claude Code to write tests for untested code — a practical workflow
Here's my testing workflow:
curl -X POST https://api.simplylouie.com/v1/chat/completions \
-H 'Authorization: Bearer YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"model":"claude-opus-4-5","messages":[{"role":"user","content":"Read these 500 lines of untested Express middleware and write Jest tests covering the happy path, error cases, and edge cases: [paste code]"}]}'
Untested code is the landmine every dev inherits. You're afraid to refactor it because you don't know what it does. You're afraid to touch it because anything might break.
Claude Code changed how I approach this. Here's my exact workflow for adding tests to legacy code with zero test coverage.
The problem with writing tests blind
When you write tests without understanding the code, you end up testing what you think the code does — not what it actually does. You miss edge cases. Your tests pass but bugs slip through.
The better approach: ask Claude to read the code first, then write tests based on observed behavior.
Phase 1: Characterization tests (what does this actually do?)
I start every untested codebase with characterization tests. These aren't about correctness — they capture current behavior, whatever it is.
> Claude, read this authentication middleware (src/auth/middleware.js).
> Don't fix anything. Just understand what it currently does.
> Then write Jest characterization tests that document its actual behavior —
> including any weird edge cases or unexpected outputs you notice.
Claude will often surface behavior you didn't know existed:
// Claude-generated characterization test
describe('auth middleware - observed behavior', () => {
it('passes req through when token is missing (unexpected!)', async () => {
// NOTE: Current code doesn't reject missing tokens — it just calls next()
// This may be a bug, but this is current behavior
const req = { headers: {} };
const next = jest.fn();
await authMiddleware(req, {}, next);
expect(next).toHaveBeenCalled();
});
it('attaches user to req when token is valid JWT', async () => {
const token = jwt.sign({ userId: 123 }, process.env.JWT_SECRET);
const req = { headers: { authorization: `Bearer ${token}` } };
await authMiddleware(req, {}, jest.fn());
expect(req.user.id).toBe(123);
});
it('does NOT call next() when token is expired', async () => {
// Token expired 1 hour ago
const token = jwt.sign({ userId: 123 }, process.env.JWT_SECRET, { expiresIn: -3600 });
const req = { headers: { authorization: `Bearer ${token}` } };
const next = jest.fn();
await authMiddleware(req, {}, next);
expect(next).not.toHaveBeenCalled();
});
});
Now you have a test suite that documents what the code does. Before you touch anything, run these. They all pass. This is your safety net.
Phase 2: Gap analysis (what's missing?)
> Here are the characterization tests you just wrote.
> Now look at the full middleware code again.
> What scenarios are NOT covered? What could go wrong that we haven't tested?
> List the missing test cases in order of risk.
Typical output:
Missing test cases (high to low risk):
1. Malformed JWT (not a valid token format) — could throw uncaught exception
2. Token with wrong signature — currently untested
3. Database down during user lookup — no error handling path tested
4. Concurrent requests with same token — race condition possible
5. Token with missing required fields (userId, role) — could cause NullPointerError downstream
This is gold. You now know exactly where the risks are before you write a single new test.
Phase 3: Write the real test suite
> Write a complete Jest test suite for this middleware that covers:
> 1. All the characterization tests above (existing behavior)
> 2. All the gap cases you identified (especially the high-risk ones)
> 3. Happy path, error cases, and edge cases
> Use describe/it blocks, mock the database calls, and include setup/teardown.
You end up with a comprehensive test suite that would have taken days to write manually.
Phase 4: Fix the bugs you found
Here's where it gets powerful. Remember that "token missing → next() called anyway" bug Claude found?
> The characterization tests revealed that missing tokens don't get rejected.
> Fix the middleware to return 401 when no Authorization header is present.
> Update the characterization test to reflect the new correct behavior.
> Make sure all other tests still pass.
Claude fixes the bug AND updates the test. Your test suite stays green.
The rate limit reality
Untested codebases are large. A typical authentication module might be 300-500 lines. A payment processor could be 1,000+ lines. When Claude reads all of that, builds test scenarios, writes the test code, then iterates on feedback — you're burning through tokens fast.
I hit the rate limit wall on a 2,000-line service layer. Halfway through writing integration tests, everything stopped.
That's when I switched to SimplyLouie — $2/month for Claude API access. No rate limits on my workflow. I can feed it the entire service layer in one session and get complete coverage.
For global developers: that's ₦3,200/month in Nigeria, ₱112/month in the Philippines, Rs165/month in India. Compare that to losing half a day every time rate limits hit.
The real win: confidence to refactor
Once you have tests, the fear goes away. You can refactor that 500-line monolith without sweating every change. Run the tests. They catch regressions instantly.
The pattern:
- Characterization tests → capture current behavior
- Gap analysis → identify missing coverage
- Full test suite → safety net in place
- Fix bugs found during testing
- Refactor with confidence
Want to try this workflow? SimplyLouie gives you Claude API access at $2/month — no rate limit interruptions, no $20/month ChatGPT pricing.
What's your biggest untested legacy module right now? Drop it in the comments.
Top comments (0)