When test coverage drops below 40%, what's your first move? A lot of developers start by scanning the codebase manually, relying on gut feel to decide what needs testing next. That instinct isn't entirely wrong — experienced developers often know which code is risky — but it doesn't scale, and it's easy to miss the edge cases that actually cause bugs in production.
If you can feed your coverage report directly to Claude Code and let it figure out what to test and why, you can skip most of that manual analysis. In one of my personal projects, I went from 45% to 78% branch coverage in under two hours using the workflow described here. The generated tests still needed review before committing — that part's non-negotiable — but the cognitive overhead of knowing where to start was essentially eliminated.
Here's how it works in practice.
What Test Coverage Actually Measures
"Coverage" means different things depending on which metric you're looking at. Vitest (backed by c8 or istanbul) tracks four main types:
- Statements: percentage of executable statements that ran during tests
- Branches: percentage of conditional branches (both sides of if/switch/ternary) that were exercised
- Functions: percentage of defined functions that were called
- Lines: percentage of source lines that were executed
Branch coverage is the one I find most meaningful for finding real bugs. A function can show 100% function coverage — meaning it was called — while its error-handling path never executes. The null checks, boundary values, and exception handlers are exactly where production incidents tend to originate, and branch coverage is what surfaces those gaps.
Statement and line coverage can give a misleadingly optimistic picture. A single happy-path test can push both metrics well above 80% in a file that has almost no meaningful verification happening.
To start tracking branch coverage in Vitest, add this to vitest.config.ts:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
// Be explicit about what to include to avoid inflated numbers
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['src/**/*.d.ts', 'src/**/index.ts'],
// Optional: fail CI if coverage drops below these thresholds
thresholds: {
branches: 60,
functions: 70,
},
},
},
})Running npx vitest run --coverage generates two things: an HTML report under coverage/ for visual review, and a coverage/coverage-summary.json that Claude Code can read directly. The JSON is the key input for everything that follows.
Analyzing the Coverage Report with Claude Code
Instead of manually cross-referencing uncovered lines with your source files, you can hand that analysis entirely to Claude Code. The prompt is straightforward:
Read coverage/coverage-summary.json and list all files
where branch coverage is below 60%.
For each file, include the file path, current branch coverage percentage,
and a brief note on which types of conditional paths are likely untested.
Claude Code reads the JSON, then opens the actual source files to understand the logic in context. It doesn't just sort the numbers — it often provides reasoning like "this error path is likely untested because all the existing tests use valid inputs" or "the null branch here is never exercised because the mock always returns a real object."
That contextual layer is what makes this different from just sorting coverage-summary.json yourself. It takes roughly 30 seconds for a project with 20-30 files, and the output gives you a prioritized list of where to focus.
Requesting Test Generation File by File
Once you have your priority list, generate tests one file at a time — not all at once. Asking Claude Code to cover ten files in a single request reduces output quality and makes it harder to verify that the generated tests match your existing conventions.
Here's a prompt structure that consistently produces usable output:
Branch coverage for src/utils/pricing.ts is at 42%.
Read this file and the existing __tests__/pricing.test.ts,
then write Vitest test cases that cover the untested branches.
Requirements:
- Append to the existing test file without duplicating describe blocks
- Cover edge cases: zero values, negative numbers, invalid currency codes
- Use describe/it blocks with descriptive names
- Write specific assertions — avoid vague ones like expect(result).toBeTruthy()
- Include the expected output as a comment if the value isn't obvious
Two things here are worth emphasizing. First, sharing the existing test file prevents naming conflicts. Second, the explicit instruction about assertion specificity matters more than it seems. Without it, you'll occasionally get tests like expect(result).not.toBeNull() that technically pass without verifying anything useful. That last line pushes Claude Code toward assertions like expect(result).toEqual({ amount: 0, currency: 'JPY' }), which actually tell you when behavior changes.
A Full Workflow: TypeScript + Vitest in Practice
Here's the complete cycle, step by step:
Step 1: Generate the coverage report
npx vitest run --coverage
# This outputs coverage/coverage-summary.jsonStep 2: Get a prioritized list from Claude Code
Check coverage/coverage-summary.json.
List the 5 files with the lowest branch coverage.
For each, describe 2-3 specific scenarios that would
most improve the numbers if tested.
Step 3: Generate tests starting with business logic
Business logic first, then error handling, then utilities. This ordering gives you the highest ROI early — tests that catch logic errors are more likely to prevent actual bugs than tests that verify utility formatting.
Step 4: Run the generated tests immediately
npx vitest run __tests__/pricing.test.tsDon't let generated tests accumulate without running them. Failures are much easier to diagnose one file at a time.
Step 5: Feed failures back to Claude Code
This test is failing with the following error:
[paste the full error output]
Identify the root cause and fix the test.
In my experience, about 20-30% of generated tests need some correction on the first run. The failures are usually mock configuration issues or incorrect expected values, not fundamental misunderstandings of the code.
Step 6: Re-run full coverage and confirm improvement
After covering two or three files, run npx vitest run --coverage again. Seeing the numbers move is motivating — and it confirms that the new tests are actually reaching the branches they were intended to cover.
Common Pitfalls and How to Handle Them
Mock setup that doesn't match your actual types
For files with external dependencies — API clients, database access, file system operations — Claude Code sometimes generates vi.mock() configurations that don't line up with your actual interfaces. The fix is simple: tell it. "This mock causes a TypeScript error. The actual interface is in src/types/api.ts." Passing the type definition resolves the issue almost every time.
Tests that pass but verify nothing meaningful
This is the most common quality issue with generated tests. Watch for assertions like expect(result).toBeTruthy() or expect(fn).not.toThrow() without checking what was returned. When you spot one, ask: "What should calculateTotal return when both quantity is 0 and discount is applied? Write an assertion for that specific case." The follow-up is usually precise.
Duplicate describe or it names causing confusing failures
If you're appending to an existing test file and the generated code reuses a describe block name that already exists, Vitest will run both but the output becomes hard to read. Passing the existing test file content in your prompt prevents this. A cat __tests__/target.test.ts in your terminal and pasting the output into Claude Code is enough.
Async tests hitting real services
Occasionally, Claude Code generates async tests that skip mock setup and make actual network or database calls. The symptom is tests that are unexpectedly slow or that fail depending on network availability. When you see this, ask explicitly: "This test is making a real HTTP call. Add vi.mock() to intercept the request and return a fixture instead."
Over-indexing on the coverage number
Branch coverage is a proxy for test quality, not a guarantee of it. It's possible to reach 90% branch coverage with tests that don't prevent any real bugs — if the assertions are weak. Use the coverage number to find untested code, but treat the review of generated tests as a separate and necessary step. The goal is tests that break when behavior changes, not tests that just run.
For deeper patterns on test structure and maintainability, the Vitest and Jest unit test automation guide covers test organization in detail, and the TDD workflow introduction is worth reading if you want to build coverage up from scratch on a new feature rather than retrofitting tests on existing code.
Your Next Step
Run npx vitest run --coverage in a project today and ask Claude Code: "List the five files with the lowest branch coverage and suggest what to test first in each." The analysis takes five minutes or less.
Coverage improvement starts with measurement. Once measurement is cheap, improvement follows naturally. With Claude Code handling the analysis and first-draft test generation, the measurement-to-fix cycle becomes fast enough to actually maintain as a habit.