AI Unit Tests for Legacy Code: Fix the Common Failures
Start with observable behavior, not generated assertions
Generating unit tests for legacy code with AI is useful when undocumented branches and tightly coupled dependencies make testing slow. The main risk is not invalid syntax: it is a plausible test that confidently enforces the wrong behavior.
Begin with characterization tests: tests that record what the code currently does. Keep these separate from tests describing what the product should do. An existing bug may deserve a temporary characterization test, but label it clearly so it does not quietly become a permanent requirement.
A repeatable workflow for AI-generated legacy tests
- Establish the baseline. Run the existing suite and record failures before adding anything. For Python projects using pytest, start with pytest -q. Confirm the interpreter, dependency versions, and required environment variables.
- Choose one small target. Prefer a calculation, parser, validator, or transformation with observable inputs and outputs. Avoid starting with a controller that touches three databases and an external billing service.
- Supply the relevant context. Include the target function, imported helpers, dependency interfaces, test configuration, and one existing test showing project conventions. Remove credentials and customer data.
- Request a behavior inventory first. Ask the model to identify branches, side effects, boundary conditions, and unanswered questions before writing tests. Resolve ambiguities instead of letting it invent requirements.
- Generate a small batch. Request three to five tests, run them locally, and review every assertion. Expand only after the initial tests demonstrate useful behavior.
A practical prompt is: Generate pytest characterization tests for this function. Do not modify production code or invent interfaces. Use existing fixtures. Cover normal, empty, boundary, and error inputs where applicable. Explain each expected result from the supplied implementation, and flag anything you cannot determine.
Why do generated tests fail immediately?
Likely cause: missing repository context. Models may guess import paths, constructor arguments, or fixture names. Give the assistant the actual module layout and the complete traceback. Ask for the smallest test-only correction, not a production rewrite that makes its guess valid.
For Python mocking, patch the dependency where the tested module looks it up. If invoices.py imports fetch_rate directly, patch invoices.fetch_rate rather than the original library export. Otherwise, the real network call may still execute.
Why do tests pass but catch nothing?
Likely cause: self-fulfilling assertions or excessive mocking. A test that mocks the function under test and then checks its mocked return value proves little. Mock external boundaries, not the business rule you want to protect.
For a discount function, check explicit results for zero quantity, the threshold quantity, and one below it. Derive expected values independently; do not call the same helper to calculate both actual and expected results. Temporarily introduce a relevant local mutation, such as changing a boundary comparison, and verify that a test fails. Revert it afterward.
How do you handle code that is difficult to isolate?
First identify controllable seams: injected clients, environment variables, file paths, clocks, and random generators. Use temporary directories instead of shared files, fixed clock values instead of the current time, and fake clients instead of live services.
If no safe seam exists, a narrow integration test may be more honest than a heavily mocked unit test. Capture behavior there before making a minimal refactor, such as extracting a pure calculation. Review production changes separately from generated tests.
FAQ: coverage, privacy, and model costs
Is high coverage enough? No. Review meaningful branches, exception behavior, and side effects. Coverage reveals execution, not whether assertions detect regressions. Use mutation testing selectively on high-risk logic.
Can I upload an entire private repository? Check your organization's policy and the data-handling terms of both the relay and model provider first. Send minimal relevant context and sanitized fixtures; never include production secrets.
How can I keep iteration affordable? 59API offers low-cost, pay-as-you-go access to native official-quality models without downgrades, including Claude Sonnet 5.5 and DeepSeek V4 Pro. It supports Claude Code, Codex, Gemini CLI, and OpenAI SDKs, with API base URL https://api.59api.com. Reuse a compact context bundle and send focused failure logs rather than repeatedly submitting the repository. Consider signing up to trial one small testing task before scaling.
When is a generated test ready to merge? When it runs reliably, verifies a meaningful outcome, introduces no hidden service dependency, and survives human review of its expected behavior.
शुरू करने के लिए तैयार?
कुछ ही मिनटों में Claude और GPT जोड़ें, सबसे कम कीमत पर। साइन अप करें और API key पाएं।
मुफ़्त साइन अप