59API

← Back to all guides

AI Unit Tests for Legacy Python: A Safe Workflow

Guides · EN · 2026-09-19

Generating tests for legacy code is difficult because the original behavior is often undocumented, inconsistent, and tied to databases or external services. AI can accelerate the work, but the useful goal is not to ask for tests blindly. The goal is to capture current behavior, identify risk, and then improve the design safely.

1. Start With a Small Legacy Function

Choose one function with a clear boundary. For example, an old invoice module might contain this behavior:

Legacy code: def calculate_total(items, customer): total = sum(item["price"] * item["quantity"] for item in items); if customer.get("vip"): total *= 0.9; return round(total, 2)

Before asking an AI model to write tests, record what matters: accepted input types, rounding rules, missing fields, empty lists, negative values, and whether the behavior is relied on elsewhere. A test that merely confirms an obvious happy path will not protect a legacy system.

2. Give the Model Focused Context

Provide the function, its callers, nearby data structures, existing test conventions, and the command used to run tests. Ask the model to separate observed behavior from assumptions. A useful prompt is:

Prompt: Analyze this Python function and generate pytest characterization tests. Preserve its current behavior, including surprising edge cases. Do not refactor production code. For every test, explain which behavior is observed from the source and which assumption requires verification. Use plain pytest and avoid external plugins.

Do not paste an entire repository into one request. Smaller context makes the generated tests easier to review and reduces the chance that the model invents APIs or fixtures.

3. Generate Tests, Then Run Them

A reasonable first result might look like this:

Test snippet: def test_regular_items(): assert calculate_total([{"price": 10, "quantity": 2}], {}) == 20; def test_vip_discount_is_rounded(): assert calculate_total([{"price": 10, "quantity": 3}], {"vip": True}) == 27

Put the tests in a dedicated characterization file and run pytest immediately. If a generated test fails, do not automatically change the implementation or the test. First determine whether the failure reveals a real contract, a misunderstanding, or a defect that production code currently exposes.

Run a narrow command such as pytest tests/test_invoice_characterization.py -q, then run the full suite. Record the baseline coverage with pytest --cov=invoice --cov-report=term-missing. Coverage is evidence about execution, not proof that the behavior is correct.

4. Add Boundary and Dependency Tests

Ask AI for cases that are easy to overlook, but verify each one against the codebase. For the example, useful cases include an empty item list, fractional prices, quantities of zero, a missing vip key, and a customer whose vip value is false.

For legacy code that calls a service, provide the real dependency interface and ask for a mock-based test. For example:

Mock snippet: def test_load_invoice_uses_repository(mocker): repository = mocker.Mock(); repository.fetch.return_value = {"total": 42}; service = InvoiceService(repository); assert service.total(7) == 42; repository.fetch.assert_called_once_with(7)

This verifies the interaction without making a network call. Be careful with mocks: asserting every internal call can freeze an implementation that you later need to change. Prefer checking externally visible results and only assert interactions that are part of the contract.

5. Use AI for Review, Not Final Authority

After the tests pass, ask the model to review them for duplicate cases, weak assertions, hidden global state, and tests that depend on execution order. A second useful prompt is: Identify tests that would pass even if the function returned a constant. Suggest stronger assertions without changing the production code.

Then use mutation testing or deliberately break a branch to check whether the suite detects the change. Once characterization tests are stable, refactor the legacy code in small commits and keep the tests running after each change.

Using 59API for Affordable Test Generation

Repeated test-generation and review requests can become expensive with standard API pricing. 59API provides pay-as-you-go access to official-quality Claude models, including Opus, Sonnet, and Haiku, as well as GPT models, without requiring a separate workflow for each provider. It works with Claude Code, Codex, and OpenAI-compatible SDKs.

For an OpenAI SDK script, point the client at https://api.59api.com:

Configuration snippet: client = OpenAI(api_key=os.environ["59API_KEY"], base_url="https://api.59api.com/v1")

Use a cheaper, faster model for first-pass test scaffolding and a stronger model for reviewing edge cases or complex fixtures. Because 59API is designed as a low-cost relay with referral rebates, it is a practical option when you need many small iterations. Sign up for 59API, set a spending limit, and keep human review as the final gate before generated tests become part of the build.

Pricing · Docs

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