Lesson 12: Testing Workflows
Testing is one of Claude Code's strongest use cases. It can write tests, run them, read failures, fix the code, and re-run — all in a single conversation. This lesson covers how to use Claude Code for test-driven development, coverage analysis, and avoiding common testing pitfalls.
Test-Driven Development with Claude
TDD follows a simple loop: write a failing test, make it pass, refactor. Claude Code is excellent at this because it can run the tests itself and iterate on failures.
Start by describing the behavior you want:
> I need a function called parseTimeRange that takes a string like
"2h30m" or "45s" and returns the total number of seconds.
Write the tests first, then implement the function.Claude will:
- Write test cases covering the expected behavior
- Run the tests (they fail because the function does not exist yet)
- Write the implementation
- Run the tests again (they pass)
- Possibly refactor for clarity
The key is telling Claude to write tests first. If you just say "write a parseTimeRange function," Claude will write the function and then (maybe) write tests that confirm what it already wrote — which defeats the purpose of TDD.
Prompting Patterns for Tests
The Coverage Prompt
Be explicit about what categories of tests you want:
> Write tests for the validateEmail function that cover:
- Happy path: valid email addresses
- Edge cases: very long addresses, subdomains, plus aliases
- Error cases: missing @, missing domain, empty string, null input
- Boundary: maximum length email addressesThe Specification Prompt
Describe behavior in terms of inputs and outputs:
> Write tests for the CartService.applyDiscount method:
- 10% discount on orders over $50 → returns discounted total
- No discount on orders $50 or under → returns original total
- Multiple discounts don't stack → only the best one applies
- Expired discount codes → throws DiscountExpiredErrorThe Regression Prompt
When fixing a bug, start with a test that reproduces it:
> We have a bug: when a user's name contains an apostrophe
(like "O'Brien"), the search function returns no results.
Write a test that reproduces this bug first, then fix it.Running Tests and Fixing Failures
Claude Code can run your test suite and interpret the results. This creates a powerful feedback loop:
> Run the test suite. If any tests fail, fix the code and re-run
until everything passes.Claude will:
- Execute your test command (e.g.,
npm test,pytest,go test ./...) - Parse the output for failures
- Read the failing test and the relevant source code
- Make a fix
- Re-run the tests
- Repeat until green
This works best when your test output is clear. If you have a custom test runner with unusual output, tell Claude how to read it:
> Run "make test". Failures show up as lines starting with "FAIL:"
followed by the test name and error message.Coverage Analysis
Claude can help you find untested code paths:
> Run the tests with coverage enabled and tell me which files
have less than 80% coverage. For each one, suggest what tests
are missing.For JavaScript projects:
> Run "npx jest --coverage" and identify untested branches
in src/services/. Write tests to cover them.For Python projects:
> Run "pytest --cov=src --cov-report=term-missing" and write tests
for the uncovered lines.Claude reads the coverage output, identifies gaps, and writes targeted tests. This is much faster than manually reading coverage reports.
Testing Anti-Patterns to Avoid
Tests That Just Pass
If you ask Claude to "write tests" after the implementation exists, it may write tests that simply confirm the current behavior — even if that behavior is wrong. Always be suspicious of tests that pass on the first run without any implementation changes.
# BAD: vague prompt after implementation
> Write some tests for the UserService
# GOOD: specific behavior-driven prompt
> Write tests for UserService.createUser that verify:
- It hashes the password before storing
- It rejects duplicate email addresses
- It returns the created user without the password fieldTesting Implementation Details
Tests should verify what the code does, not how it does it:
# BAD: tests that break when you refactor
> Test that processOrder calls database.insert exactly once
with the correct SQL query string
# GOOD: tests that verify behavior
> Test that processOrder creates an order record that can be
retrieved by ID and has the correct totalOver-Mocking
Claude sometimes creates elaborate mock setups that make tests pass but do not actually test anything meaningful:
> Write integration tests for the checkout flow that use a real
test database, not mocks. I want to verify the actual behavior
end-to-end.Example: Full TDD Cycle
Here is a complete TDD session for a utility function:
> I need a function called slugify that converts a string to a
URL-friendly slug. Requirements:
- Lowercase everything
- Replace spaces with hyphens
- Remove special characters except hyphens
- Collapse multiple hyphens into one
- Trim hyphens from start and end
Use TDD: write the tests first in tests/slugify.test.js,
run them to see them fail, then implement in src/utils/slugify.js,
then run tests again to see them pass.Claude will produce something like:
// tests/slugify.test.js
import { slugify } from '../src/utils/slugify.js';
describe('slugify', () => {
test('converts to lowercase', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('replaces spaces with hyphens', () => {
expect(slugify('foo bar baz')).toBe('foo-bar-baz');
});
test('removes special characters', () => {
expect(slugify('hello! @world#')).toBe('hello-world');
});
test('collapses multiple hyphens', () => {
expect(slugify('foo---bar')).toBe('foo-bar');
});
test('trims leading and trailing hyphens', () => {
expect(slugify('--hello--')).toBe('hello');
});
test('handles empty string', () => {
expect(slugify('')).toBe('');
});
});Then the implementation, then a green test run. This is TDD done right — the tests defined the behavior before the code existed.
Unit Tests vs Integration Tests
Guide Claude on which type you need:
# Unit test — isolated, fast, focused
> Write unit tests for the calculateShipping function.
Mock the database and API calls.
# Integration test — real dependencies, slower, more realistic
> Write an integration test for the order creation flow.
Use the test database and real HTTP requests to the API.Unit tests are better for individual functions and business logic. Integration tests are better for verifying that components work together. A good test suite has both.
Key Takeaways
- Tell Claude to write tests before implementation for true TDD
- Be explicit about test categories: happy path, edge cases, error cases, boundaries
- Let Claude run tests, read failures, and fix code in a loop — this is where it excels
- Use coverage analysis to find gaps, then have Claude write targeted tests
- Avoid tests that just confirm current behavior — specify the expected behavior yourself
- Test behavior, not implementation details, so tests survive refactoring
- Use unit tests for isolated logic and integration tests for end-to-end flows