
If you've been shipping code without tests because writing them takes too long, this post is for you. I'll show you exactly how I use Claude Code to generate complete Jest test suites — edge cases included — in under a minute, and why it actually sticks compared to the "I'll add tests later" approach.
The Problem: Tests Are the Work Nobody Does
Here's the honest situation: test-driven development sounds great in theory. In practice, after building a feature, writing tests for it feels like doing the dishes twice. The feature works, you know it works, you just watched it work — so you move on.
The debt compounds. Three months later the codebase is untested and now every change ships with a silent prayer that nothing breaks in production. Refactoring becomes a horror show because there's no safety net.
The gotcha isn't that developers don't know how to write tests. It's that test writing is slow and tedious, especially for utility functions that have five or six code paths. That's exactly the kind of mechanical work Claude Code handles well.
Section 1: The Baseline Command
Open any source file and run one command. Here's the exact prompt I use:
claude "Write Jest test code for all functions in this file. Include both normal cases and exception cases."
On a 50-line utility file, Claude Code generates the test file in under 30 seconds. In my testing on an n8n Mac environment, roughly 95% of the generated tests passed without any edits — the remaining 5% were minor mock adjustments.
Here's what that looks like end-to-end for a calculator utility:
// utils/calculator.js — the source file
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return a + b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
module.exports = { add, divide };
After running the command, Claude generates:
// calculator.test.js — generated by Claude Code
const { add, divide } = require('./utils/calculator');
describe('add', () => {
test('returns sum of two positive numbers', () => {
expect(add(3, 7)).toBe(10);
});
test('handles negative numbers', () => {
expect(add(-5, 3)).toBe(-2);
});
test('throws TypeError when arguments are not numbers', () => {
expect(() => add('a', 1)).toThrow(TypeError);
expect(() => add(null, 1)).toThrow(TypeError);
});
});
describe('divide', () => {
test('returns quotient of two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws Error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Run it:
npx jest calculator.test.js
Expected output:
PASS calculator.test.js
add
✓ returns sum of two positive numbers (3ms)
✓ handles negative numbers (1ms)
✓ throws TypeError when arguments are not numbers (2ms)
divide
✓ returns quotient of two numbers (1ms)
✓ throws Error when dividing by zero (1ms)
Test Suites: 1 passed, 1 passed
Tests: 5 passed, 5 passed
Section 2: Getting Claude to Think About Edge Cases
The default prompt covers happy paths and obvious exceptions. For more complete coverage, I use a second pass specifically for edge cases:
claude "Reinforce the tests focusing on edge cases. Must include null, undefined, and empty array cases."
What Claude typically adds that I'd forget:
| Edge Case | Why It Matters |
|---|---|
null input |
Common in real data pipelines |
undefined parameter |
Missing function arguments |
Empty array [] |
Off-by-one errors, reduce() edge cases |
| Boundary values (0, -1, MAX_INT) | Fencepost bugs |
| Async rejection paths | Unhandled promise errors |
Here's what that looks like in practice for an array utility:
// utils/arrayUtils.js
function getFirst(arr) {
return arr[0];
}
function sumAll(arr) {
return arr.reduce((acc, val) => acc + val, 0);
}
Edge-case-focused test output:
describe('getFirst', () => {
test('returns first element', () => {
expect(getFirst([1, 2, 3])).toBe(1);
});
test('returns undefined for empty array', () => {
expect(getFirst([])).toBeUndefined();
});
test('handles null input gracefully', () => {
expect(() => getFirst(null)).toThrow();
});
});
describe('sumAll', () => {
test('sums positive numbers', () => {
expect(sumAll([1, 2, 3])).toBe(6);
});
test('returns 0 for empty array', () => {
expect(sumAll([])).toBe(0);
});
test('handles negative numbers', () => {
expect(sumAll([-1, -2, 3])).toBe(0);
});
});
The core insight here: Claude doesn't just mirror your function signatures — it reasons about what realistic failure inputs look like. The empty-array case on sumAll returning 0 (not crashing) is exactly the kind of boundary behavior that bites you in production aggregation pipelines.
Section 3: Applying This to Existing Code
The biggest unlock for me was using this on legacy code — files that have been running in production for months with zero test coverage, so nobody wants to touch them.
claude "Analyze the utils/calculator.js file and create a new test file calculator.test.js"
You can also point at multiple files:
claude "Create test files for all .js files in the utils/ directory. Follow the existing naming convention *.test.js"
Gotchas I Hit
Mocks for external dependencies: If your function calls a database or makes HTTP requests, Claude will generate tests but the mocks might not match your actual setup. Review any jest.mock() calls carefully and update the module paths.
// Claude might generate this
jest.mock('../db/connection');
// But your project might use
jest.mock('../../config/database');
Async functions: Make sure Claude adds async/await or .resolves/.rejects matchers. If a test passes synchronously when the function is async, you have a false positive. Double-check:
// Fragile — might pass even if the promise rejects
test('fetches user', () => {
expect(fetchUser(1)).toBeDefined();
});
// Correct
test('fetches user', async () => {
const user = await fetchUser(1);
expect(user.id).toBe(1);
});
Framework differences: If you're not on Jest, just swap the framework name in your prompt:
# Vitest (Vite projects)
claude "Write Vitest test code for all functions in this file"
# pytest (Python)
claude "Write pytest tests for all functions in this file, including edge cases"
# Go testing
claude "Write Go table-driven tests for all exported functions in this file"
Environment Differences
| Environment | What to watch for |
|---|---|
| Mac local | Path separators in mocks — use path.join() |
| Linux/Docker | No Windows CRLF issues, but check file permissions on generated files |
| CI (GitHub Actions) | Add --ci flag to Jest to prevent watch mode from hanging |
# .github/workflows/test.yml
- name: Run tests
run: npx jest --ci --coverage
Closing
The bottom line: test coverage is no longer a skill gap problem. It's a time and motivation problem — and Claude Code removes both. Point it at a file, give it one line of instruction, and you have a working test suite in under a minute.
What's next from here: once tests exist, you can ask Claude to set up a pre-commit hook that runs them automatically, so broken code never reaches your repo. That's the "bike with brakes" moment this unlocks — not just having tests, but having them enforced.
🐦 Faster updates on X: @baegseungh7061
📚 More in this series: Code Intro
💌 Subscribe: Follow on X or grab the RSS
댓글
댓글 쓰기