Module 12 · Lesson 04
Refactoring and Test Generation
Reading time: 18 minutes Track: Claude Fluency for Teams · Developer path
Why refactoring and testing are Claude's sweet spot
Refactoring and test generation share a property that makes them ideal for Claude Code: the success criteria are objective and verifiable. A refactored function either has the same behavior or it doesn't. A test either passes or fails. This eliminates the subjective judgment problem that makes some AI coding tasks risky.
Refactoring workflows
Pattern 1: Explaining what you want to improve
Don't just ask for "better" code. Specify what quality you're improving and why.
Refactor this function to improve readability. Specifically:
- Break it into smaller functions with clear names
- Extract magic numbers as named constants
- Remove the nested ternaries — replace with early returns or clear conditionals
Preserve all current behavior. Do not change the function signature.
Pattern 2: Targeted extraction
This function has a section (lines 45-78) that handles [specific concern] mixed in
with [main concern]. Extract the [specific concern] section into a separate function.
The new function should be pure (no side effects).
Pattern 3: Type improvements (TypeScript)
This function uses 'any' in three places. Replace each with appropriate types.
For the return type, infer it from the implementation. For the input parameters,
use the narrowest type that's correct.
Explain each type you add.
Pattern 4: Performance refactoring
This function is called thousands of times per second and profiling shows it's a bottleneck.
Analyze it and propose a performance refactoring. Trade code clarity for performance
only where the gain is significant.
Always verify behavior preservation
After any refactoring, explicitly verify behavior:
Run the existing tests for this module. They should all pass after the refactoring.
If any fail, identify whether the test is testing an internal detail (can be updated)
or actual behavior (the refactoring changed something unintentionally).
Test generation workflows
Generating comprehensive unit tests
Generate unit tests for [function/module]. Cover:
- Happy path with typical inputs
- Boundary conditions (empty, null, max/min values)
- Error cases (what should throw, what should return gracefully)
- Any edge cases you identify from reading the implementation
Use [Vitest/Jest/pytest/your framework] syntax.
Name each test descriptively — the name should tell you what behavior it's verifying.
Tests-from-spec
When you have a specification but no implementation yet:
Here is the specification for the [function/module]:
[paste spec]
Write tests that would verify an implementation meets this spec.
Write the tests before any implementation exists — they should initially fail.
Adding test coverage to legacy code
This module has no test coverage. Read the implementation and generate tests
that document the current behavior — not what it should do ideally, but what it
currently does. These are our safety net before we refactor.
Flag any behavior that looks like a bug you wouldn't want to preserve.
This last pattern is particularly powerful for legacy codebases: generate characterization tests that document current behavior, then refactor with confidence that behavior is preserved.
The review discipline for generated tests
Generated tests need the same review as generated code:
- Read every test and confirm it's testing what the name says it tests
- Check that error cases are actually testing the error behavior (not accidentally passing)
- Verify the test would fail if you introduced the bug it's meant to catch
- Make sure tests are isolated (no dependencies between tests, no shared mutable state)
A quick way to validate: ask Claude to temporarily introduce a specific bug and confirm the relevant test fails. If it doesn't, the test isn't actually catching what it claims to catch.