DEV Community

Dakota Huang
Dakota Huang

Posted on

Characterization Tests Need a Home: Run Them on a Free Server

Characterization tests turn messy code into a refactor target. They record current behavior before you change anything. Then you can refactor with confidence.

Here's the workflow I use for those scary legacy functions. It runs on a free server, so you get a baseline without standing up a CI machine.

The Problem: Refactoring Without Tests

Messy code has hidden behavior. You see one branch, but there are edge cases in the shadows.

You change a line. A feature breaks. Nobody knows why.

The solution is simple: freeze the behavior first. Write tests that assert what the code actually does, not what it should do.

Step 1: Pick the Messiest Function

Choose a function with many branches, side effects, or copied logic. For this example, let's use a price calculator.

def calculate_price(item_type, quantity, discount_code):
    base = 10 if item_type == "book" else 20
    if quantity > 5:
        base = base * 0.9
    if discount_code == "SAVE10":
        base = base * 0.9
    return round(base * quantity, 2)
Enter fullscreen mode Exit fullscreen mode

This function has two magic numbers and two hidden rules. Perfect for characterization.

The goal is to capture every input–output pair you can think of.

Step 2: Write a Behavior Capture Harness

Create a script that feeds a set of inputs to the function and logs the outputs. This harness is your oracle.

import json

cases = [
    ("book", 3, ""),
    ("book", 6, ""),
    ("book", 6, "SAVE10"),
    ("game", 1, ""),
    ("game", 10, "SAVE10"),
]

results = {}
for item, qty, code in cases:
    key = f"{item}:{qty}:{code}"
    results[key] = calculate_price(item, qty, code)

with open("baseline.json", "w") as f:
    json.dump(results, f, indent=2)

print("Baseline captured")
Enter fullscreen mode Exit fullscreen mode

Run this script before you touch anything. Save the output as baseline.json.

That file is your contract.

Step 3: Move the Harness to a Free Server

You don't need to run this locally. You can host it on a free server and run it on a schedule.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode offers free model access and a free server tier. The free tier includes 10 million tokens for AI usage and a small always-on server. That's enough to run a cron job that re-captures behavior daily.

Here's a simple Dockerfile for your harness:

FROM python:3.11-slim
WORKDIR /app
COPY harness.py .
CMD ["python", "harness.py"]
Enter fullscreen mode Exit fullscreen mode

Deploy this container to MonkeyCode's free server. Add a cron rule to run it every morning.

Now you have a living baseline. If someone changes the function, the next run detects the difference.

Step 4: Make the Smallest Safe Change

Now you refactor. Keep the change atomic. Extract a helper, rename a variable, or simplify a condition.

For our function, we can extract the discount logic:

def apply_discount(price, code):
    if code == "SAVE10":
        return price * 0.9
    return price

def calculate_price(item_type, quantity, discount_code):
    base = 10 if item_type == "book" else 20
    if quantity > 5:
        base = base * 0.9
    return round(apply_discount(base, discount_code) * quantity, 2)
Enter fullscreen mode Exit fullscreen mode

Run the harness again. Compare the new baseline.json with the old one.

If the outputs match, your refactor is behavior-preserving. If they differ, you've found a behavior change before it reaches production.

Step 5: Automate the Comparison

Don't compare manually. Write a simple diff script:

python harness.py
python -c "
import json
old = json.load(open('baseline.json'))
new = json.load(open('current.json'))
print('Match' if old == new else 'Mismatch')
"
Enter fullscreen mode Exit fullscreen mode

On your free server, a failing diff can email you or post to a Slack webhook.

Now every deployment candidate gets checked against the frozen behavior.

Limitations and Who Should Skip This

This workflow assumes you have a function you can call directly. If the code is deeply coupled to network I/O or a database, you need a different harness.

It also only covers the inputs you thought to test. Weird edge cases will still hide.

Characterization tests are not a replacement for real unit tests. They are a safety net for legacy code you don't understand yet.

Skip this if you already have good coverage. Skip it if the code is trivial and safe. Use it when the code is messy and scary.

The Takeaway

Characterization tests let you refactor with facts, not vibes.

A free server makes the baseline continuous. You don't have to babysit a local script.

Try this flow on your worst function. You'll learn more about it in an hour than in a week of reading.

Top comments (0)