# Write characterization tests for untested code before a refactor

> Pin down what legacy code actually does with a partition table, SUSPECT flags for odd behavior, and a runnable test file, before anyone changes it.

- **Author:** [Chidi Adeyemi (@chidi_adeyemi)](https://promptabide.com/chidi_adeyemi)
- **Tested on:** Claude · Opus 5.5
- **You fill in:** `code`, `framework`
- **Published:** 2026-09-02
- **Updated:** 2026-09-24
- **Tags:** `coding`, `testing`, `refactoring`
- **Keywords:** write unit tests for legacy code with ai, characterization tests prompt, pytest tests for untested function, generate edge case tests with chatgpt, test coverage before refactoring
- **Views:** 979
- **Likes:** 16

**Best for:** Developers inheriting untested functions who need a safety net before refactoring or fixing them.

## Prompt

```
Write tests for this untested code before anyone refactors it. The goal is to pin down its CURRENT behavior, including behavior that looks wrong.

Code:
{{code}}

Test framework: {{framework}}

Steps:
1. List the input partitions as a table: partition | example input | output according to the code as written. Cover normal cases, each boundary (the value on each side of every comparison), empty/None values, and error paths.
2. Trace each row through the code yourself. Don't guess outputs.
3. Mark any row where the current behavior looks like a bug with SUSPECT and one sentence saying why. Do not fix it in the test. Assert what the code actually does, and leave a comment so a human can decide.
4. Write the test file. One behavior per test, names that read as sentences, no mocks unless the code does I/O.
5. Say which branches or conditions are still uncovered, if any.
```

## Variables

- `{{code}}` — The function or class to test, pasted in full (e.g. shipping_fee() from checkout/fees.py)
- `{{framework}}` — Test framework and any conventions (e.g. pytest, plain asserts, parametrize where it helps)

## Example input

````
framework: pytest, plain asserts, use parametrize where it keeps things short
code:
```python
# checkout/fees.py
def shipping_fee(subtotal, weight_kg, country, express=False):
    if subtotal >= 999:
        base = 0
    elif country == "IN":
        base = 49
    else:
        base = 499
    if weight_kg > 5:
        base += int(weight_kg - 5) * 20
    if express:
        base = base * 2 if base else 99
    return base
```
````

## Output

*Produced by Claude · Opus 5.5.*

````
**1–3. Partitions (traced by hand)**

| Partition | Input (subtotal, kg, country, express) | Output |
|---|---|---|
| Free-shipping threshold, exact | 999, 1, "IN" | 0 |
| Just below threshold | 998.99, 1, "IN" | 49 |
| International, below threshold | 500, 1, "US" | 499 |
| International, above threshold | 1500, 1, "US" | 0 **SUSPECT**: free international shipping may be meant for India only |
| Lowercase country | 500, 1, "in" | 499 **SUSPECT**: "in" is charged as international |
| Weight exactly 5 | 500, 5, "IN" | 49 |
| Weight 5.9 | 500, 5.9, "IN" | 49 **SUSPECT**: `int()` floors the extra 0.9 kg, so no surcharge |
| Weight 7.5 | 500, 7.5, "IN" | 89 |
| Free order, heavy | 1200, 8, "IN" | 60 |
| Express, paid order | 500, 1, "IN", True | 98 |
| Express, free order | 1200, 1, "IN", True | 99 **SUSPECT**: costs more than express on a paid order |
| Express, free and heavy | 1200, 8, "IN", True | 120 |

**4. Test file**
```python
# tests/test_shipping_fee.py
import pytest
from checkout.fees import shipping_fee

@pytest.mark.parametrize("subtotal, expected", [(999, 0), (998.99, 49)])
def test_free_shipping_starts_at_999(subtotal, expected):
    assert shipping_fee(subtotal, 1, "IN") == expected

def test_international_orders_below_threshold_pay_499():
    assert shipping_fee(500, 1, "US") == 499

def test_international_orders_above_threshold_ship_free():
    # SUSPECT: free-shipping rule ignores country
    assert shipping_fee(1500, 1, "US") == 0

def test_lowercase_country_is_treated_as_international():
    # SUSPECT: no normalization of country code
    assert shipping_fee(500, 1, "in") == 499

@pytest.mark.parametrize("kg, expected", [(5, 49), (5.9, 49), (7.5, 89)])
def test_weight_surcharge_is_20_per_whole_kg_over_5(kg, expected):
    # SUSPECT at 5.9: partial kilograms are floored
    assert shipping_fee(500, kg, "IN") == expected

def test_free_orders_still_pay_weight_surcharge():
    assert shipping_fee(1200, 8, "IN") == 60

def test_express_doubles_a_paid_fee():
    assert shipping_fee(500, 1, "IN", express=True) == 98

def test_express_on_a_free_order_costs_flat_99():
    # SUSPECT: more than express on a paid domestic order (98)
    assert shipping_fee(1200, 1, "IN", express=True) == 99

def test_express_doubles_the_surcharge_on_free_heavy_orders():
    assert shipping_fee(1200, 8, "IN", express=True) == 120
```

**5. Coverage**
Every branch is exercised. Nothing covers invalid input: negative weights, `None` for country, or a string subtotal. The function has no validation, so decide whether that belongs here or at the caller before adding tests for it.
````

## Why it works

Characterization tests only work if they record **what the code does, not what it should do**, and the prompt says so twice. Without that, models quietly "fix" bugs in their expected values and your safety net fails on day one. The **partition table with boundaries on each side of every comparison** is what surfaced the 5.9 kg flooring and the 98 vs 99 express oddity. **"Trace each row yourself"** cuts down on invented outputs. **SUSPECT flags** turn the test run into a bug list for a human to triage, without changing behavior mid-refactor.

## When not to use it

It's a poor fit for code whose behavior depends on databases, clocks or network calls you can't paste. There, you need integration tests or recorded fixtures, not traced partitions. For very large functions the hand-tracing gets unreliable. Split the function first, or run the generated tests and correct the expected values from real results.

---

Canonical HTML: https://promptabide.com/bides/characterization-tests-for-untested-code
Agent guide: https://promptabide.com/llms.txt · https://promptabide.com/agent-instructions.md
Sitemap: https://promptabide.com/sitemap.xml
