DEV Community

Dakota Huang
Dakota Huang

Posted on

The Smallest Safe Change: A Branch-Level Refactor Workflow for Messy Repos

Most refactors die quietly while the test suite stays green. The bug ships because the change was too wide. A safe refactor touches one branch at a time.

Characterization tests lock that branch down before you move. This workflow works even on messy legacy repos. I used MonkeyCode's free model for one discovery step.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free server option gives a disposable runtime for the harness. Neither tool is required, but both help.

Why Branch Size Matters

A line is not a decision point in a diff. One line can still alter three separate branches. The reverse is also true: twenty lines can change no branch.

The safe unit is a branch, not a line. Count every direct if condition in the function. Each refactor should change at most one branch.

The Workflow

I will walk through a messy Python module. The function carries a hidden global side effect. It is a perfect target for this technique.

Step 1: Pick a Sealed Unit

A useful sealed unit returns a plain value. It may mutate a known global list. It must not read time, randomness, or the environment.

# legacy.py
messages = []

def apply_surcharges(base: float, zone: str, weight_class: str) -> float:
    if zone == "remote":
        messages.append("remote")
        base *= 1.2
    if weight_class == "heavy" and zone != "local":
        messages.append("heavy")
        base += 15.0
    if base > 500:
        messages.append("big")
        base -= 10.0
    return round(base, 2)
Enter fullscreen mode Exit fullscreen mode

This function contains three independent conditional branches. The third depends on the mutated base. The side effect list must be locked too.

Step 2: Build a Boundary Grid

The branch base > 500 demands a boundary test. Include 500 and 501 in your price grid. Use local, remote, and other as zones.

Use two weight classes for better coverage. The Cartesian product gives you a complete grid. The script then records the current behavior.

# record.py
import itertools
import json
import legacy

prices = [100, 500, 501]
zones = ["local", "remote", "other"]
weights = ["light", "heavy"]

cases = list(itertools.product(prices, zones, weights))
data = []

for base, zone, weight in cases:
    legacy.messages = []
    result = legacy.apply_surcharges(base, zone, weight)
    data.append({
        "base": base,
        "zone": zone,
        "weight": weight,
        "result": result,
        "messages": legacy.messages
    })

with open("characterization.json", "w") as f:
    json.dump(data, f, indent=2)
Enter fullscreen mode Exit fullscreen mode

Record the state before you think about refactoring. This JSON file is your refactoring contract. It is an honest snapshot of current bugs.

Step 3: Lock the State With a Test

The test reads the recorded JSON snapshot file. It re-runs the original function and checks the output. It compares both the return and the side effect list.

# test_legacy.py
import json
import pytest
import legacy

with open("characterization.json") as f:
    recorded = json.load(f)

@pytest.mark.parametrize("case", recorded)
def test_characterization(case):
    legacy.messages = []
    result = legacy.apply_surcharges(
        case["base"], case["zone"], case["weight"]
    )
    assert result == case["result"]
    assert legacy.messages == case["messages"]
Enter fullscreen mode Exit fullscreen mode

Run the test once and it passes cleanly. Now you have a reliable behavioral oracle in place. The oracle now knows the entire legacy behavior.

Step 4: Ask a Model for a Branch Map

I copied the function into MonkeyCode's free model. I asked for a complete branch inventory list. The model returned a surprisingly useful breakdown.

Here is a condensed version of that result. Treat the model output as a testable hypothesis. Verify every line against the source code.

- `zone == "remote"` -> multiply by 1.2, log "remote"
- `weight_class == "heavy" and zone != "local"` -> add 15, log "heavy"
- `base > 500` -> subtract 10, log "big"
Enter fullscreen mode Exit fullscreen mode

The model did not need to be perfect. I verified each branch against the actual code. The returned inventory matched the actual source code.

Step 5: Apply One Small Refactor

The smallest safe change here is an extraction. Extract the third branch into a helper function. Keep the behavior exactly as it existed before.

# legacy_refactored.py
messages = []

def _lower_big_orders(base: float) -> float:
    if base > 500:
        messages.append("big")
        return base - 10.0
    return base

def apply_surcharges(base: float, zone: str, weight_class: str) -> float:
    if zone == "remote":
        messages.append("remote")
        base *= 1.2
    if weight_class == "heavy" and zone != "local":
        messages.append("heavy")
        base += 15.0
    return round(_lower_big_orders(base), 2)
Enter fullscreen mode Exit fullscreen mode

This extraction touches exactly one logical branch only. The condition and the handler stay identical. The only change is the new function location.

Step 6: Re-run and Compare

Point the test at the refactored module. The recorded JSON snapshot remains unchanged. The full test should pass immediately.

$ pytest test_legacy.py
Enter fullscreen mode Exit fullscreen mode

A green test means the old behavior is preserved. You have now completed a safe atomic change. Repeat the full process for the next branch.

Why This Method Avoids Surprises

Each committed step is fully reversible at any point. The characterization suite stays green throughout the process. You can stop after any step and ship the code.

That property is the real and practical benefit. It turns a risky refactor into safe commits. You never bet the codebase on one giant diff.

Why the Boundary Matters

Try this simple experiment after the refactor. Change base > 500 to base >= 500. The test will fail and point to the 500 case.

E   AssertionError: assert 491.0 == 500.0
E   assert ['big'] == []
Enter fullscreen mode Exit fullscreen mode

That test failure makes the entire point clear. Without the boundary, that subtle bug hides. Your characterization suite must cover every branch edge.

Where the Free Tier Helps

MonkeyCode's free model access reduces the boring work. It can sketch inventories and suggest refactors quickly. The free server option gives you a clean disposable runtime.

Even without MonkeyCode, the branch map is manual. The model only accelerates that first pass. The server runtime is a convenience for clean experiments. Your characterization tests remain the source of truth.

This simple workflow avoids polluting your local environment. Both tools are convenient, but they are strictly optional. The real safety comes from the locked contract.

Limitations

Characterization tests freeze the entire current legacy behavior. If the current code is broken, you freeze a bug. That state is fine for a pure refactor.

The workflow is wrong for planned behavior changes. The entire workflow assumes deterministic, repeatable code. Random time, I/O, or threading breaks the oracle.

Concurrency creates another serious problem in legacy code. Parallel state changes make the recorded list unsafe.

Who Should Not Use This

Do not use this workflow during a migration. If the old behavior is intentionally wrong, do not preserve it. Do not use it on inherently nondeterministic code.

Do not use it to justify a huge architectural rewrite. The method works for slow and surgical steps. That limitation is the entire design point here.

Conclusion

A refactor is a series of small safe changes. Each change needs its own independent proof. Characterization tests provide the necessary evidence for each change.

First, record the current behavior with boundary inputs. Then, change exactly one branch at a time. Finally, re-run the entire characterization suite and compare.

The smaller the step, the easier the review. Free tools can speed up the tedious busywork. The locked contract keeps your application code honest. Try this on your messiest legacy function this week.

Top comments (0)