Refactoring safely with tests
Restructure code without changing its behavior, and prove that behavior is unchanged.
def shipping_before(weight, express):
if express == True:
if weight <= 1:
return 15
else:
if weight <= 5:
return 25
else:
return 40
else:
if weight <= 1:
return 5
else:
if weight <= 5:
return 10
else:
return 20
RATES = [(1, 5, 15), (5, 10, 25)]
def shipping_after(weight, express):
for limit, standard, fast in RATES:
if weight <= limit:
return fast if express else standard
return 40 if express else 20
cases = [(w, e) for w in (0, 1, 1.5, 5, 5.1, 50) for e in (False, True)]
assert all(shipping_before(w, e) == shipping_after(w, e) for w, e in cases)
print(len(cases), "cases identical")
print(shipping_after(3, True), shipping_after(30, False))12 cases identical 25 20
Explanation
Refactoring means changing the internal structure of code to make it easier to understand and change, without changing what it does. It is done in small steps, and after each step you check that behavior is still the same. Without checks, refactoring turns into risky rewriting.
The safety net is tests. When a function has no tests, write characterization tests first: record what it currently returns for a representative set of inputs, including boundaries, then restructure. Here the original shipping function is a pyramid of nested conditions that repeats the weight limits. The new version expresses the same rules as a small table that is easy to read and extend.
The program compares the old and the new function on every combination of weight and shipping speed in a table of cases that includes the boundary weights 1 and 5. If any answer differs, the assertion fails. Notice that refactoring did not add a feature: when you find a bug or want new behavior, do that as a separate change so each change is easy to review.
Explore the dry run
def shipping_before(weight, express): if express == True: if weight <= 1: return 15 else: if weight <= 5: return 25 else: return 40 else: if weight <= 1: return 5 else: if weight <= 5: return 10 else: return 20 RATES = [(1, 5, 15), (5, 10, 25)] def shipping_after(weight, express): for limit, standard, fast in RATES: if weight <= limit: return fast if express else standard return 40 if express else 20 cases = [(w, e) for w in (0, 1, 1.5, 5, 5.1, 50) for e in (False, True)]assert all(shipping_before(w, e) == shipping_after(w, e) for w, e in cases)print(len(cases), "cases identical")print(shipping_after(3, True), shipping_after(30, False))Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The weight limits and prices now live in one table instead of being repeated in nested ifs. | — | — | |
| One expression replaces four branches of the old code. | — | — | |
| The characterization check compares both versions on all 12 inputs, including the boundary weights 1 and 5. | — | — | |
| Behavior is preserved on every tested case. | — | 12 cases identical | |
| A 3 kg express parcel costs 25 and a 30 kg standard parcel costs 20, as before. | — | 12 cases identical 25 20 |
Cost and common mistakes
The refactored function does O(r) comparisons for r rate rows, which is a constant for a fixed table. The old version used a fixed number of comparisons.
- Refactoring without tests is guessing. Create tests that pin down current behavior before you change the structure.
- Do not mix refactoring with new features or bug fixes in one change; it makes reviews and rollbacks hard.
- Equal results on tested inputs do not prove equality on all inputs. Choose inputs at boundaries and in every branch.
Try it yourself
Add a third tier: weights up to 10 cost 15 standard and 30 express, and above that 20 and 40. Which line changes?
Reveal answer and reasoning
Add (10, 15, 30) to RATES after (5, 10, 25) and change the final fallback prices to 20 and 40. The loop needs no changes, which is the benefit of the table.