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)
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")
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"]
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)
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')
"
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)