Claude CodeLesson 10 of 25

Writing & Running Tests

Testing is not just something you add at the end — it is a workflow. Claude Code can write tests before the code exists, run your entire suite, diagnose failures, and fix the code or the test. This lesson covers the full loop.

🧪

Two Modes: Generate vs TDD

You can use Claude Code for testing in two distinct ways. Use both — they serve different moments in the build cycle.

Generate tests for existing code

You have a function or module already written. Ask Claude to cover it with tests — happy path, edge cases, and error conditions.

“Write tests for @src/lib/format.ts — happy path, null input, empty string.”

TDD — test first, code second

Describe what you want to build. Claude writes failing tests first, then writes the implementation to make them pass.

“Write failing tests for a formatCurrency(amount, currency) function, then implement it.”

🔁

The TDD Loop

Test-driven development follows a tight five-step cycle. Claude Code speeds up every step — writing the test, writing the code, and fixing failures.

📝

Write the test

🔴

Run — it fails

🛠️

Write the code

🟢

Run — it passes

♻️

Refactor & repeat

The red step is intentional. A test that fails before you write the code proves the test is actually checking something real — not just always passing.
1

Example A — Generate Tests for Existing Code

You have a formatCurrency utility already written. Ask Claude to write full coverage.

The existing function

src/lib/format.ts
export function formatCurrency(amount: number, currency = 'USD'): string {
if (isNaN(amount)) throw new Error('Invalid amount');
return new Intl.NumberFormat('en-US', {
style: 'currency', currency
}).format(amount);
}
Y

Write Vitest tests for @src/lib/format.ts. Cover: happy path (positive number), zero, negative number, a non-USD currency, and the NaN error case. Put the tests in src/__tests__/format.test.ts.

I'll write 5 tests covering all those cases. Each test will import formatCurrency directly and use expect() assertions.

Vitest — format.test.ts
formats positive USD amount (3ms)
formats zero as $0.00 (1ms)
formats negative amount (1ms)
formats EUR currency (2ms)
throws on NaN input (1ms)
5 passed5 totalALL PASS
Tip: always name the cases explicitly in your prompt (“zero, negative, non-USD, NaN”) rather than asking for “full coverage.” Claude produces more targeted, readable tests when you name the scenarios.
2

Example B — TDD From Scratch

You want a slugify utility but have not written it yet. Use TDD: tests first, implementation second.

Phase 1 — Write failing tests
Y

Write failing Vitest tests for a slugify(text: string): string function that does not exist yet. It should: lowercase the text, replace spaces with hyphens, strip special characters, and trim leading/trailing hyphens. Save to src/__tests__/slugify.test.ts. Do NOT create the implementation yet.

I'll write the tests first — they will all fail since slugify doesn't exist. This confirms the tests are actually testing something.

Vitest — slugify.test.ts (before implementation)
lowercases text
ReferenceError: slugify is not defined
replaces spaces with hyphens
ReferenceError: slugify is not defined
strips special characters
ReferenceError: slugify is not defined
trims leading/trailing hyphens
ReferenceError: slugify is not defined
4 failed0 passed4 total

4 failures — expected. The function does not exist yet.

Phase 2 — Implement to pass
Y

Now create src/lib/slugify.ts — implement slugify() so that all tests in @src/__tests__/slugify.test.ts pass. Do not change the tests.

I'll implement slugify in src/lib/slugify.ts to satisfy every test case without touching the test file.

src/lib/slugify.ts
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
Vitest — slugify.test.ts (after implementation)
lowercases text (2ms)
replaces spaces with hyphens (1ms)
strips special characters (1ms)
trims leading/trailing hyphens (1ms)
4 passed4 totalALL PASS
Key rule: always tell Claude “do not change the tests” when asking it to implement the function. Otherwise it may make tests pass by weakening the assertions instead of writing correct code.
3

Running the Full Suite

Claude Code can run your entire test suite and report results back. Ask it to run tests from inside the Claude session — or do it yourself in the integrated terminal.

Y

Run the full test suite and tell me which tests are failing.

Running npm run test now. I'll report the results and highlight any failures.

Claude Code — full suite run
# Claude runs: npm run test
src/__tests__/format.test.ts
5 tests passed
src/__tests__/slugify.test.ts
4 tests passed
src/__tests__/tasks.test.ts
2 tests passed
11 tests passed — 0 failed

For continuous feedback while you work, run Vitest in watch mode in a separate terminal:

Terminal — watch mode
$ npx vitest --watch
Watching for file changes⬦ (press q to quit)
Re-running tests for: src/lib/slugify.ts
4 tests passed
4

Fixing a Failing Test

A test breaks after a code change. Paste the failure into Claude and it will diagnose whether the code is wrong or the test needs updating.

Terminal — unexpected failure
formatCurrency ⬺ formats zero as $0.00
AssertionError: expected '$0.00' to equal '0'
1 test failed — 10 passed
Y

Test failing after my last change to @src/lib/format.ts: AssertionError: expected '$0.00' to equal '0'. The test expects '$0.00' but the function now returns '0'. Which is wrong — the test or the code?

The code is wrong. Zero is a valid currency amount and should be formatted as '$0.00'. Your recent change removed the Intl.NumberFormat call for zero values. I'll fix format.ts — the test should not change.

Golden rule: when a test breaks after a code change, the code is almost always the problem — not the test. Only change a test if the requirements genuinely changed. Ask Claude to confirm which is wrong before letting it touch the test file.

Prompting Patterns That Work

The quality of Claude's tests depends heavily on how you ask. These patterns consistently produce useful, readable tests.

GoalPrompt pattern
Cover an existing function"Write Vitest tests for @src/lib/x.ts — cover happy path, [list edge cases]."
TDD — tests first"Write failing tests for [describe function]. Do NOT implement it yet."
TDD — implement"Implement [function] so all tests in @src/__tests__/x.test.ts pass. Do not change the tests."
Fix a failure"Test failing: [paste error]. File: @src/lib/x.ts. Is the code or the test wrong? Fix accordingly."
Run the suite"Run npm run test and report which tests are failing and why."
Add a missing case"Add a test to @src/__tests__/x.test.ts for the case where [describe scenario]."

Works with Any Test Framework

Claude Code adapts to whatever framework your project already uses. Just name it in the prompt or rely on CLAUDE.md to carry the convention.

Vitest

TypeScript / Vite

Jest

JavaScript / Node

Pytest

Python

Go test

Go

Add your test framework to CLAUDE.md once: Tests: Vitest, colocated in src/__tests__/. Every session knows which framework and file location to use without being told.

Testing Workflow Quick Reference

Name the test cases explicitly

happy path, zero, null, error — not just "full coverage"

Always @-mention the file under test

Claude reads the source before writing tests

Keep tests and source in sync in CLAUDE.md

so Claude always uses the right framework and path

In TDD: test first, implement second

confirm the tests fail before asking Claude to implement

Never let Claude weaken assertions to pass tests

add "do not change the tests" to the implement prompt

Paste the exact error when fixing failures

Claude diagnoses the right cause much faster with the full error

Run watch mode while building

npx vitest --watch gives instant feedback on every save

What's Next

Your test coverage is growing. The final Claude Code lesson distils the best practices — habits and patterns that make Claude Code more effective every day.