Legacy code resists change. You know it by the fear in every PR review. The fix is not more courage. It is a measurable behavior baseline, and a rule that says: change nothing until the baseline exists.
This workflow uses free resources end-to-end. No new stack. No big upfront investment. You write characterization tests, run them against a free server, and make the smallest safe edit that keeps the fingerprint identical.
Why characterization tests first
Characterization tests lock current behavior. They do not assert what you wish the code did. They assert what it actually does, bugs and all.
Identification, not correction, is the goal. A characterization test tells you when behavior changes, not whether the change is good.
For a messy repo, this is gold. You get a red/green signal before you touch the logic.
Step 1: Choose the module and record its boundary
Pick a high-fan-in, low-quality module. Internal utilities and public handlers work best. Steer clear of UI glue and hard-to-isolate side effects.
Write a short boundary list:
- function: apply_discount(cart, code)
- inputs: cart dict, code string, optional user tier
- outputs: new total, applied discount, error list
- side effects: none
This list becomes your test plan.
Step 2: Generate test candidates with a free model
Copy that boundary list into a free coding model, for example the one bundled inside MonkeyCode's free tier. Ask for brute-force input/output pairs only, not assertions about intended behavior.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Example request:
Generate 20 raw test cases for apply_discount. Do not judge correctness. Just cover empty cart, duplicated codes, unavailable codes, negative quantities, and mixed item types.
The model returns candidates. You do not trust them. You verify them by running.
Step 3: Run a baseline on a free server
Free server options are common now. I used a free instance for this run. The point is to get execution results without paying for your laptop's uptime.
Set up a tiny runner:
# fingerprint.py
import hashlib, json
import your_module
def capture(func, case):
try:
out = func(**case["inputs"])
return {"ok": True, "out": out}
except Exception as exc:
return {"ok": False, "out": str(exc)}
def fingerprint(cases):
lines = []
for c in cases:
res = capture(your_module.apply_discount, c)
digest = hashlib.sha256(json.dumps(res, sort_keys=True).encode()).hexdigest()
lines.append(digest)
return "\n".join(lines)
Run it once and save the output as baseline.txt. This file is your behavior fingerprint.
Step 4: Prove the tests catch change
A baseline that never moves is useless. You need to know your tests detect refactors.
Introduce a deliberate mutation. For example, reverse the discount eligibility condition. Run the fingerprint again.
python fingerprint.py > current.txt
diff baseline.txt current.txt
You should see mismatches. If you do not, your test cases are too weak. Add more until mutations change the fingerprint.
Rule of thumb: three distinct mutations, three changed fingerprints. Otherwise, the suite is not safe to drive refactoring.
Step 5: The minimal safe change loop
Now the actual refactor works in tiny steps:
- Reorder one internal helper. Run the fingerprint.
- Extract a repeated block. Run the fingerprint.
- Rename variables. Run the fingerprint.
- Touch one behavior only. Run the fingerprint and inspect the diff.
The order matters. Structural changes first, behavioral changes last.
What the workflow protects
- Silent logic flips get caught immediately.
- Refactors stay reviewable because each diff is small.
- The team gains a permanent regression net after the dust settles.
Limitations and when not to use this
This approach does not fix design. It fixes safety.
- It cannot detect conjunctions of multiple changes.
- It only covers exercised input space. Blind spots remain.
- It will lock in bugs. If the code is wrong today, your baseline says wrong is correct.
Do not use it for greenfield code with clear contracts. Write real unit tests there.
Do not use it when the module has huge external side effects. Your fingerprint will not capture database writes or network calls cleanly.
And do not expect a free server to handle expensive integration suites. Keep the baseline small and focused.
The takeaway
Characterization tests give you the floor. Minimal safe changes keep you from falling through it. Free models generate the candidates, and a free server runs the baseline. The whole loop costs you time, not money.
Try it on your messiest module this week. Start with the boundary list, not the refactor.
Top comments (0)