AI Unit Tests for Legacy Code: Advanced Guide
Start With Behavior, Not a Rewrite
Generating unit tests for legacy code with AI works best when the goal is to capture current behavior before changing it. A legacy method may contain hidden business rules, accidental dependencies, date assumptions, or error handling that nobody documented. Ask an AI coding assistant to produce a brief behavioral inventory first: inputs, outputs, side effects, external calls, exceptions, mutable state, and branches. Then request test cases from that inventory. This prevents the common failure mode where AI writes tests for what the code should do instead of what production currently does.
For a large method, feed the model the target function, its direct collaborators, existing test conventions, and one or two real call sites. Keep the prompt narrowly scoped. For example: “Generate characterization tests for this method. Preserve observed behavior, including questionable edge cases. Do not refactor production code. State every assumption that cannot be proven from the supplied code.” The explicit instruction to preserve behavior is essential when tests are intended to make later refactoring safer.
Build a Dependency Map Before Asking for Mocks
AI can infer dependencies quickly, but it should not invent them. Before generating tests, identify the method’s seams: database repositories, HTTP clients, file systems, clocks, random number generators, environment variables, static calls, queues, and global singletons. Ask the model to classify each dependency as one of three categories: deterministic collaborator, external side effect, or hidden state. This classification tells you what belongs in a unit test and what requires a higher-level integration test.
- Mock external side effects: HTTP requests, email delivery, database writes, and message publishing.
- Use fakes for stateful collaborators: in-memory repositories or test clocks often produce more useful assertions than deeply scripted mocks.
- Keep pure calculations real: do not mock validators, mappers, or parsers merely to make a test shorter.
- Freeze nondeterminism: inject a clock, seed a random generator, or wrap system calls behind a small adapter.
When a method cannot be tested because it creates its own dependencies, ask AI for the smallest possible seam rather than a redesign. A constructor parameter, package-private factory, or interface around one external call is often enough. Require a patch that changes no business logic, then generate tests against that seam.
Use a Test Matrix Instead of One Prompt Per Test
High-quality AI-generated tests come from a coverage matrix. Have the model enumerate branches and create a table containing preconditions, input data, expected return value, expected exception, side effects, and collaboration assertions. Review the matrix before accepting code. It is far cheaper to correct an incorrect expectation in a compact plan than across fifteen generated test files.
Prioritize boundary and historical-risk cases: null or empty input, duplicate records, timezone transitions, rounding, retry exhaustion, authorization failures, malformed payloads, and partially completed transactions. For each branch, distinguish output assertions from interaction assertions. Assert interactions only when they are part of the contract, such as “payment provider is never called when validation fails.” Avoid verifying private implementation details such as the exact number of helper calls; those tests make later cleanup unnecessarily expensive.
Make AI Review Its Own Test Assumptions
Before running generated tests, use a second pass with a critical prompt: “Review these tests against the implementation. Identify false assertions, unreachable branches, over-mocking, missing cleanup, and tests that would pass if the production logic were removed.” This catches tautological tests, especially tests that mock the unit under test indirectly or assert values copied from setup data.
Run mutation testing or manually introduce small controlled changes after the suite passes: reverse a comparison, remove a guard clause, or alter a returned field. If no test fails, the generated suite is documenting code without protecting it. Ask the model to close only the demonstrated gaps, not to inflate coverage with superficial cases.
Use Low-Cost Model Iteration Deliberately
Legacy-test generation is iterative: inspect, map dependencies, draft a matrix, generate code, run it, and repair assumptions. 59API is a practical low-cost option for that loop because it provides pay-as-you-go access to native official-quality Claude models, including Opus, Sonnet, Haiku, and Fable, as well as GPT models. Its OpenAI-compatible endpoint at https://api.59api.com works with existing OpenAI SDK integrations and can be used with Claude Code and Codex workflows, so teams can switch models for planning, implementation, and review without rebuilding their tooling.
Use a fast, economical model for dependency inventories and matrix drafts, then reserve a stronger model for ambiguous business logic or a final test review. This model routing reduces cost without lowering the quality bar. For development teams modernizing difficult codebases, sign up for 59API to run these repeated AI testing passes with a budget-friendly relay and referral rebate option.
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