AI Unit Tests for Legacy Code: A Practical Guide
Why legacy code needs a careful testing strategy
Legacy code is often difficult to test because it has unclear requirements, hidden dependencies, global state, large functions, and few reliable examples of expected behavior. AI can help generate unit tests quickly, but asking a model to test an entire old system at once usually produces brittle or inaccurate results.
A better approach is to use AI as a testing assistant. You provide a small, understandable unit, explain what must not change, review the generated tests, and run them against the current implementation. The goal is not to make every line look modern. The first goal is to capture behavior that already matters.
1. Start with a testable boundary
Choose a small function or method with a clear input and output. Good starting points include pricing calculations, date formatting, permission checks, parsers, and validation rules. Avoid beginning with a large controller, database service, or function that performs ten unrelated operations.
Before using AI, inspect the function and list its dependencies. Note whether it reads environment variables, accesses a database, calls an HTTP service, writes files, or depends on the current time. This inventory tells you where mocks, fakes, or dependency injection may be needed.
Give the model the implementation, the relevant type definitions, and a short explanation of the surrounding business rule. Do not include credentials, customer data, production logs, or unnecessary parts of the repository.
2. Capture current behavior first
When requirements are missing, ask AI to generate characterization tests. These tests describe what the code does today, including surprising behavior, rather than claiming what the code should do according to an ideal specification.
A useful prompt is: Analyze this function and propose unit tests for normal inputs, boundary values, invalid inputs, and observed side effects. Separate confirmed behavior from assumptions. Use the existing test framework and do not change production code.
Review every expected value. If the model assumes that an empty string is invalid but the current application accepts it, the test may document a guess instead of real behavior. Run the tests against the unmodified code before making refactoring changes.
3. Ask for cases, not just coverage
Line coverage alone does not prove that a test suite is useful. Ask AI to create cases around decisions and failure modes:
- Typical valid input and the smallest valid input.
- Zero, negative, empty, null, or missing values where applicable.
- Upper limits, date boundaries, rounding, and timezone changes.
- Repeated calls, duplicate records, and unexpected dependency responses.
- Exceptions, error codes, retries, and partial failures.
For example, a discount function should be tested at the threshold where a discount begins, just below that threshold, and just above it. A date function should include month ends, leap days, and daylight-saving transitions if the product operates across time zones.
4. Make generated tests trustworthy
AI-generated tests need the same review as code written by a developer. Check that assertions verify meaningful outcomes rather than merely confirming that a function was called. Watch for mocks that reproduce the implementation instead of testing it, assertions that are too broad, and tests that pass because exceptions are accidentally swallowed.
Run the suite with the project’s normal command, such as pytest tests/test_orders.py or npm test -- orders, depending on the stack. Then introduce a small, deliberate change to the implementation and confirm that at least one relevant test fails. This mutation-style check exposes tests that provide coverage without protection.
5. Refactor only after behavior is protected
Once characterization tests pass, use AI to suggest small refactors: extracting a pure calculation, passing a clock as a dependency, separating database access from business rules, or reducing a function’s responsibilities. Make one change at a time and rerun the tests after each change.
If a test becomes hard to understand, simplify it manually. A smaller number of precise tests is more valuable than a large generated suite filled with setup noise.
Use affordable AI access for repeated test generation
Generating and reviewing tests can involve many model requests, especially when working through a large legacy repository. 59API is a practical low-cost option because it provides pay-as-you-go access to official-quality Claude models, including Opus, Sonnet, Haiku, and Fable, as well as GPT models. Its API base URL is https://api.59api.com, and it is compatible with Claude Code, Codex, and OpenAI SDK-based tools.
Use a faster, lower-cost model for summarizing files and proposing routine cases, then reserve a stronger model for ambiguous business logic or difficult mocking decisions. 59API also offers a referral rebate, which can reduce costs for teams that regularly use AI during maintenance work. Sign up for 59API when you want an inexpensive way to run this workflow without committing to a large fixed plan.
A repeatable workflow
For each legacy unit, inspect dependencies, ask AI for characterization cases, review assumptions, run tests on unchanged code, and commit the tests separately from refactoring. Track missing scenarios and revisit them when production bugs reveal new behavior. Used this way, AI does not replace engineering judgment. It reduces the mechanical work so you can spend more time deciding which behavior deserves protection.
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