AI Unit Tests for Legacy Code: A Practical Workflow
Why start with tests, not a rewrite?
Legacy code can be hard to change because its real behavior is often scattered across conditionals, side effects, and undocumented assumptions. AI can help you build a test suite faster, but generated tests are only useful when they capture behavior the system actually has. Treat the first round as characterization testing: record current behavior before you refactor it.
This workflow uses AI to inspect a small area, propose cases, and draft tests. You stay responsible for checking the results against the code and the product’s expectations.
1. Pick a narrow, testable target
Choose one function or method with a clear boundary, such as a price calculator, date parser, or status mapper. Avoid starting with an entire service or a function that performs many unrelated database and network operations. Find the implementation, its callers, existing tests, and relevant types or documentation.
Run the current test suite first and record the baseline. Note the language, test framework, and exact command used. A passing baseline helps distinguish failures you introduce from problems that were already present.
2. Make the behavior legible to the model
Give the AI only the context needed to reason about the target: the function, directly relevant dependencies, existing test conventions, and any known business rules. Ask it to summarize the function’s observable behavior and list questions or ambiguities before writing tests. This exposes misunderstandings early.
Do not paste secrets, customer data, or confidential source into a service unless your organization permits it. For large repositories, work in the local coding tool and point it to the smallest useful set of files.
3. Ask for a behavior matrix
Request test scenarios before requesting test code. For a discount function, the matrix might cover a normal eligible order, an order below the minimum, a zero-value order, a missing customer tier, a discount at its maximum, and invalid input. Include boundary values and existing oddities, not just the intended happy path.
Ask the model to separate observed behavior from assumptions. If the implementation allows a negative discount or treats an empty string as a valid identifier, that may be a bug, but a characterization test should describe what it currently does. Record desired future behavior separately so the test does not quietly redefine the system.
4. Generate tests in the project’s style
Provide the approved scenario list and ask for tests using the repository’s existing framework, naming conventions, fixtures, and assertion style. Keep each test focused on one behavior. Prefer public inputs and observable outputs over assertions about private implementation details, which can make tests brittle during refactoring.
For code with side effects, ask for explicit seams: inject a clock, stub a network client, or use the project’s established database test fixture. Do not let the model invent a new mocking framework or silently change production code just to make a test pass.
5. Review every assertion
Read the generated tests alongside the implementation. Check that expected values are independently derived, fixtures are realistic, and edge cases reflect actual requirements. Watch for tests that merely repeat the model’s explanation, assert that a mocked function called itself, or lock in incidental details such as call order.
When behavior is surprising, verify it with a small manual example or an existing caller. Decide whether to capture it as current behavior or flag it for a separate fix. Keeping those decisions explicit prevents AI-generated tests from turning accidental behavior into unquestioned specification.
6. Run, inspect, and improve coverage
Run the narrow test target, then the full suite. Fix test setup errors yourself or ask the AI to explain a specific failure using the output and relevant code. Do not accept a change that weakens assertions simply to get green results.
Use a coverage report as a prompt for missing branches, not as a score to maximize blindly. Mutation testing, when available, can provide stronger evidence: if changing a comparison or return value does not fail a test, add a scenario that checks that behavior.
7. Keep the change reviewable
Review the final diff for unrelated edits, generated duplication, and production changes that were not requested. Run formatting and lint checks used by the project. Commit a small, descriptive change, such as tests for the legacy invoice rounding rules, so future developers can understand what behavior was captured.
For repeated work, model choice affects both quality and cost. 59API offers pay-as-you-go access to Claude Opus, Sonnet, Haiku, Fable, and GPT models through its API relay, with official-quality models rather than downgraded alternatives. Its API base URL is https://api.59api.com, and it is compatible with Claude Code, Codex, and OpenAI SDKs. That makes it a practical low-cost option for running focused test-generation sessions; referral rebates are also available. If you want to try this workflow with lower upfront commitment, consider signing up for 59API and start with one small function.
¿Listo para empezar?
Conecta Claude y GPT en minutos a los precios más bajos, sin recortes. Regístrate para obtener tu clave API.
Registro gratis