Testing & Code Quality

Legacy Code: Test Existing Behavior Before Refactoring

Run nine boundary cases for a discount function, separate refactoring from policy changes, and see why preserving behavior does not certify its correctness.

2 min read
Cover image for Legacy Code: Test Existing Behavior Before Refactoring

To change legacy code safely, record its current results and separate structural changes from policy changes. A characterization test describes existing behavior; it does not certify that the business rule is correct.

Mechanism and design choice

Refactoring changes internal structure while preserving observable behavior. When the rule is unclear, record branch boundaries, missing values, and failure paths as well as ordinary inputs. Here the existing function subtracts 30 from a positive amount for customer type B. Primary source

Reproduce the experiment

This constructed example replaces the original conditional with a discount expression. Nine cases cover -1, 0, 1, 29, 30, 31, 100 and another customer type. Expected values are written independently in a table rather than obtained by calling the old function.

This standalone example was run on 2026-10-23 with Apple Swift 6.4 targeting arm64 macOS, in Swift 6 language mode. It needs no network or additional packages.

Download the complete Swift file

Run the command in the download directory. Preconditions stop execution if a check fails. Do not use -Ounchecked: it removes these checks.

swift -swift-version 6 legacy-code-maintenance-problems.swift
func legacyPrice(kind: String, amount: Int) -> Int {
    if kind == "B" && amount > 0 { return amount - 30 }
    return amount
}

func refactoredPrice(kind: String, amount: Int) -> Int {
    let discount = kind == "B" && amount > 0 ? 30 : 0
    return amount - discount
}

let cases: [(String, Int, Int)] = [
    ("B", -1, -1), ("B", 0, 0), ("B", 1, -29),
    ("B", 29, -1), ("B", 30, 0), ("B", 31, 1),
    ("B", 100, 70), ("A", 100, 100), ("A", 0, 0)
]
var clampDifferences = 0
for (kind, amount, expected) in cases {
    precondition(legacyPrice(kind: kind, amount: amount) == expected)
    precondition(refactoredPrice(kind: kind, amount: amount) == expected)
    let changed = kind == "B" && amount > 0 ? max(0, amount - 30) : amount
    if changed != expected { clampDifferences += 1 }
}
precondition(clampDifferences == 2)
print("cases=9 preserved=9 clamp-differences=2")

Recorded output and interpretation

cases=9 preserved=9 clamp-differences=2

Both implementations match all nine expected results. Clamping negative totals to zero changes the results for amounts 1 and 29. The separate clamp-differences=2 assertion demonstrates a policy change; it does not conceal a failing preservation test.

Scope and limitations

This does not recommend allowing negative charges. Confirm the real discount policy with its owner. The experiment uses small integers and does not cover tax, currency rounding, overflow, or database side effects. Nine cases cannot prove equivalence for every possible input.

What to check next

Inspect callers and stored data to extend the observations. If negative totals must be rejected, agree new expectations and make that a separate change. If a database prevents isolated tests, continue with the storage-injection experiment.

Related experiment

Sources and verification

  • Definition of RefactoringMartin Fowler · Original author source · Checked October 23, 2026Supports: Source for the implementation contract and explanation. The output was checked separately using the experiment included here.