Legacy Code Unit Tests with AI: Zero to Production
Why legacy code needs a different testing strategy
Generating unit tests for a new module is relatively straightforward: you know the intended behavior, the dependencies are designed for substitution, and failures are usually actionable. Legacy code is different. Its behavior may be undocumented, side effects may be mixed into business logic, and callers may depend on bugs that nobody wants to change yet.
AI can accelerate the work, but the first objective is not to create ideal tests. It is to capture the behavior that exists today. These are often called characterization tests. Once they pass consistently, you can refactor with evidence instead of guesswork.
1. Prepare a small, inspectable target
Start with one function, class, or module rather than giving an AI tool the entire repository. Choose code that has a clear input and output, such as a pricing calculator, parser, serializer, or permission check. Record the language, test framework, build command, and the module's direct dependencies.
Before generating anything, run the existing test suite and save the baseline result. Also identify external boundaries: databases, HTTP clients, file systems, clocks, random number generators, queues, and environment variables. These boundaries determine what must be mocked or replaced with fakes.
2. Ask AI to investigate before writing tests
Give the model the implementation, nearby types, relevant callers, and existing tests. Ask it to explain the observable behavior, list branches, identify side effects, and point out unknown assumptions. Tell it not to modify production code yet.
A useful prompt is: Analyze this module as legacy code. List normal cases, boundary values, error paths, state changes, external calls, and behaviors that may be accidental but observable. Separate facts supported by the code from assumptions requiring verification.
Review this analysis yourself. AI can miss implicit contracts, especially when behavior is defined by database constraints, framework conventions, or poorly named flags.
3. Generate characterization tests
Next, ask for tests that describe current behavior using the repository's existing conventions. Require one behavior per test, deterministic inputs, explicit assertions, and isolated external dependencies. Ask the model to avoid broad snapshots unless the output is genuinely a stable contract.
- Cover the simplest valid input first.
- Add boundary values such as empty strings, zero, maximum lengths, missing fields, and duplicate records.
- Exercise every meaningful branch, including exceptions and fallback behavior.
- Assert important side effects, such as calls, writes, emitted events, and transaction decisions.
- Use descriptive test names that state the condition and observed result.
Have the AI explain why each test exists. Reject tests that merely execute lines without checking a meaningful result.
4. Use AI economically with 59API
Test generation can require many iterations: inspect the code, draft tests, run failures, provide error output, and refine the suite. 59API is a practical low-cost relay for this workflow because it offers pay-as-you-go access to native official-quality Claude and GPT models through the API base URL https://api.59api.com.
You can use Claude Sonnet or Haiku for repository analysis and routine test drafts, then use Opus or a capable GPT model for difficult dependency graphs and subtle behavioral questions. 59API is among the cheapest relays, supports Claude Code and Codex, and works with OpenAI-compatible SDKs, so you can keep your existing tooling while controlling spend. Its referral rebate can further reduce the cost for teams that use the service regularly. Sign up for 59API when you are ready to run the workflow against a real repository.
5. Run, review, and improve the generated suite
Run the narrow test command first, then the full suite. When a test fails, give the model the exact failure, stack trace, and relevant implementation without asking it to blindly change assertions. Decide whether the failure reveals an incorrect test, an environment problem, or a real defect.
Check for brittle mocks, assertions tied to private implementation details, order-dependent tests, and accidental network or database access. Run the suite repeatedly and, where available, use mutation testing to verify that tests detect realistic changes.
6. Move safely to production
Commit tests separately from refactoring. Add them to continuous integration with the same commands used locally, and require deterministic cleanup for temporary files, database records, and patched environment variables. Track coverage as a diagnostic rather than a target: branch coverage and mutation results are more informative than a single percentage.
After the characterization suite is stable, refactor one risk area at a time. Keep the tests passing, add focused tests for newly clarified requirements, and mark behavior that should deliberately change. AI is most valuable here as a fast reviewer and test-design partner; the production contract remains a human decision.
Ready to get started?
Connect Claude & GPT in minutes at the lowest prices — full-power, never downgraded. Sign up to get your API key.
Sign up free