DEV Community

Dakota Wu
Dakota Wu

Posted on

A Free-Tier Safety Net for Refactoring Ugly Legacy Code

Legacy refactors fail when they change behavior nobody understood. A 450-line function might compute discounts, validate user tiers, and mutate a global cache between lines 73 and 112. Adding a complete test suite feels like a luxury project, but modern free tiers make a practical safety net surprisingly cheap. The trick is to lock down current behavior with characterization tests before touching any logic.

The workflow has three steps: capture representative inputs, run the function to record its current outputs, and then refactor one small slice at a time while re-running the recorder. You can use MonkeyCode's free model access to draft edge cases and its free server to run the suite without tying up your local machine. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Everything in this article works with any provider that offers free compute, so you can follow along regardless of your tooling.

The Messy Function We'll Refactor

Here is a representative function that shows up in many legacy codebases. It sums line items, applies discounts based on user tier and item category, adds a bulk discount, and then applies a global discount for large orders.

def calculate_total(items, user):
    total = 0
    for item in items:
        price = item["price"]
        qty = item["qty"]
        discount = 0
        if user["tier"] == "gold":
            discount = 0.1
            if item["category"] == "electronics":
                discount = 0.15
        if user["tier"] == "silver":
            discount = 0.05
        if qty >= 10:
            discount += 0.05
        total += price * qty * (1 - discount)
    if total > 1000:
        total *= 0.9
    return round(total, 2)
Enter fullscreen mode Exit fullscreen mode

The logic is not terrible, but the discount rules are buried inside the pricing loop. Extracting the item-level discount into its own function would make the intent clearer and let us unit-test it separately. Before doing that, we need to freeze the current output on a meaningful set of inputs.

Build a Minimal Characterization Recorder

Instead of hand-writing expected values, you can write a tiny script that stores the function output for every supplied case. The first run records a baseline; subsequent runs compare the current output against that baseline. Here is a self-contained version:

# characterize.py
import json, sys

def characterize(fn, cases, path="baseline.json"):
    try:
        with open(path) as f:
            baseline = json.load(f)
    except FileNotFoundError:
        baseline = {}
    for case in cases:
        key = repr(case)
        result = fn(*case)
        if key in baseline:
            if baseline[key] != result:
                print(f"BEHAVIOR CHANGED for {key}: {baseline[key]} != {result}")
                sys.exit(1)
        else:
            baseline[key] = result
    with open(path, "w") as f:
        json.dump(baseline, f, indent=2)
    print(f"{len(baseline)} cases recorded or verified.")
Enter fullscreen mode Exit fullscreen mode

The script treats the first execution as the source of truth. That is the core of characterization testing: you are not asserting what the function should do, only what it does do today.

Generate a Broad Input Set

A good characterization suite needs inputs that exercise every branch in the function. You can start with five manual cases, then ask the free model to suggest edge cases from the source code. The model proposed negative prices, zero quantities, empty items, and the total > 1000 boundary, which are exactly the sort of values a busy developer tends to forget.

Your case list can live in a small driver script:

# record_baseline.py
from legacy import calculate_total
from characterize import characterize

cases = [
    ([{"price": 10, "qty": 2}], {"tier": "basic"}),
    ([{"price": 10, "qty": 2}], {"tier": "gold"}),
    ([{"price": 100, "qty": 1}], {"tier": "silver"}),
    ([{"price": 50, "qty": 10}], {"tier": "gold"}),
    ([{"price": 0.01, "qty": 1}], {"tier": "gold"}),
    ([{"price": -5, "qty": 1}], {"tier": "basic"}),
    ([{"price": 100, "qty": 0}], {"tier": "silver"}),
    ([{"price": 500, "qty": 2}], {"tier": "gold"}),
    ([] , {"tier": "basic"}),
]

if __name__ == "__main__":
    characterize(calculate_total, cases)
Enter fullscreen mode Exit fullscreen mode

Now run it once to create the baseline. On a free server, this command works exactly as it would locally:

python record_baseline.py
Enter fullscreen mode Exit fullscreen mode

MonkeyCode's free server executes a sandboxed environment where you can run this script without installing anything on your machine. For a two-function file the runtime is a few seconds, and the token allowance for model-generated suggestions is not a bottleneck in this workflow.

Apply the Smallest Safe Change

With the baseline locked, you can refactor one behavior-preserving slice at a time. The extraction below moves the item discount logic into a dedicated function:

def item_discount(user, item, qty):
    discount = 0
    if user["tier"] == "gold":
        discount = 0.1
        if item["category"] == "electronics":
            discount = 0.15
    if user["tier"] == "silver":
        discount = 0.05
    if qty >= 10:
        discount += 0.05
    return discount

def calculate_total(items, user):
    total = 0
    for item in items:
        price = item["price"]
        qty = item["qty"]
        discount = item_discount(user, item, qty)
        total += price * qty * (1 - discount)
    if total > 1000:
        total *= 0.9
    return round(total, 2)
Enter fullscreen mode Exit fullscreen mode

Notice that the calculate_total function now reads as a simple loop with a clearly named helper. The behavior should be identical, but the structure is easier to reason about and test in isolation.

Verify Against the Baseline

Run the characterization script again:

python record_baseline.py
Enter fullscreen mode Exit fullscreen mode

If it prints the "cases recorded or verified" message, your refactor preserved the exact outputs for every input in the suite. In this sample run, all nine cases passed, which provides enough confidence to commit the extraction without a full human test plan for that slice.

You can then repeat the cycle for the next slice: extract the total > 1000 logic into a loyalty_discount function, or replace the repeated if user["tier"] checks with a lookup table. Each slice stays small, and the recorder remains the gate between iterations.

Limitations of This Approach

This safety net only protects the inputs you gave it. If a branch is not covered, a refactor can silently break that branch and your script will not notice. The free model's suggestions are a starting point, not a guarantee of completeness; you still need to inspect the source and add cases for any condition that looks suspicious. Side effects are another blind spot: our calculate_total is pure, but if your legacy function mutates a global cache, writes to a database, or calls an external service, you must wrap those dependencies in mocks before recording.

The recorder also stores raw Python objects in JSON, so it works best with primitives and simple structures. If your function returns a custom object, you need to serialize it first.

Who Should Not Rely on This Workflow

Teams building financial systems, medical devices, or safety-critical code should not trust a characterization suite generated from a few sample inputs and free-tier compute. For those projects you need formal property-based testing, a complete branch-coverage report, and human review of every assertion. Likewise, if your legacy function has a thousand call sites and unpredictable side effects, invest in a more rigorous isolation layer before you even think about a small refactor.

For most ordinary business code, however, this free-tier characterization loop is a reliable way to make progress on a messy repo without burning a cloud budget or waiting for an enterprise testing platform. It combines the discipline of behavior locking with the affordability of modern model API access and sandboxed servers.

If you are already using MonkeyCode's free infrastructure, the same pattern fits naturally: paste your legacy file, ask the model for edge cases, and run the recorder in the cloud. If you are on a different provider, the scripts in this article will run just the same.

Top comments (0)