AI Unit Tests for Legacy Code: A Practical 2026 Guide
Generating unit tests for legacy code with AI works best when you treat the model as a test author whose assumptions need verification. Older applications often hide business rules in database queries, exception handlers, and undocumented edge cases. In 2026, the practical goal is to capture important behavior, identify uncertain requirements, and build a reliable regression suite before changing implementation.
Start with a narrow, valuable target. Choose one function or small module that changes frequently, handles money, or has caused production incidents. Run the existing suite first and record failures. Confirm the test command, runtime version, and installed test framework so the assistant follows your repository instead of inventing a new setup.
For a Python project using pytest, supply the target module, relevant callers, existing fixtures, dependency configuration, and a small example of the repository's testing style. Include sanitized input and output examples where available. Exclude credentials, customer records, and unrelated files. Focused context reduces token costs and makes proposed tests easier to review.
Ask for a behavior inventory before test code. A useful prompt is: “Inspect calculate_invoice and its callers. List observable behaviors, boundary values, external dependencies, and ambiguities. Cite the relevant functions. Do not modify production code. Separate behavior visible in the implementation from assumptions that need confirmation.”
Review that inventory against documentation, support tickets, and domain knowledge. If the code calculates tax before applying a discount, a generated test might faithfully preserve that behavior even when the business expects the opposite. Characterization tests record current behavior; they do not establish that it is correct. Mark disputed behavior explicitly before treating it as a requirement.
Generate small tests with independently checked expectations. Suppose calculate_invoice accepts line items, a discount percentage, and a tax rate. Give the assistant a reviewed example: two items totaling 100.00, a 10% discount, then 20% tax should produce 108.00 under the agreed calculation order. Ask it to use the application's decimal type and rounding policy.
- Cover a normal invoice and an empty invoice.
- Test zero discount, the maximum allowed discount, and values outside that range.
- Include a rounding boundary, with the expected amount calculated independently.
- Check documented exceptions and whether input objects remain unchanged.
Request one clear behavior per test, descriptive names, and explicit expected values. Avoid tests that compute their expected result by repeating the production algorithm: the same mistake can then appear on both sides of the assertion.
Control dependencies at the boundary. Legacy code often reads the clock, queries a database, or sends an HTTP request inside business logic. Ask the model to identify those interactions before selecting mocks. Freeze time where the test framework supports it, replace network clients with controlled fakes, and patch symbols where the module looks them up. Avoid mocking every internal helper, which makes harmless refactoring break tests.
If the function cannot run without initializing the whole application, consider a small, reviewed seam, such as accepting a clock or repository argument. Keep production edits separate from generated tests so reviewers can assess each change. Use integration tests for behavior that depends on real database constraints or transactions.
Validate the assertions, not just the green result. Run the focused tests, then the relevant existing suite. Check that tests pass individually and in different orders. Inspect fixtures for shared state and ensure execution requires no live network access. Coverage can reveal untouched branches, but it cannot prove that assertions detect incorrect behavior.
Use targeted mutation testing to challenge important cases. Temporarily change a discount boundary or remove a rounding operation: a relevant test should fail. If it survives, investigate whether the assertion is weak, the path is untested, or the mutation leaves behavior equivalent. Review mutations instead of chasing a score.
Keep iteration affordable. 59API offers low-cost, pay-as-you-go access to native official-quality models without downgrades. You can try Claude Sonnet 5.5 or GPT-6 Astra through workflows compatible with Claude Code, Codex, Gemini CLI, and OpenAI SDKs, using https://api.59api.com as the API base URL. Send focused context, reuse your reviewed behavior inventory, and request small test batches to control spending. Sign up for 59API when you are ready to try this workflow on a contained legacy module.
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