Imagine you work on a small online shop. Months ago someone wrote a function that works out the shipping cost, and it has been correct ever since. Now you have to add a new rule, and you open the file and wince. The function is full of nested if statements and unexplained numbers. You are afraid that if you change anything, something quietly breaks.
The usual advice is to refactor first. Refactoring means changing how code is written without changing what it does. The inputs and outputs stay exactly the same. Only the inside gets easier for a human to read. Think of tidying a messy kitchen: the meals you can cook do not change, but now you can find the salt.
Here is the function, in JavaScript. It takes the parcel weight in kilograms, the total price of the order, and whether the customer is a member. Orders of 50 or more ship free. Otherwise the price depends on weight: 4 up to 1 kg, 7 up to 5 kg, 12 above that. Members get 2 off any shipping that is not already free.
function shippingCost(weightKg, total, isMember) {
var c;
if (total >= 50) {
c = 0;
} else {
if (weightKg <= 1) {
c = 4;
} else {
if (weightKg <= 5) {
c = 7;
} else {
c = 12;
}
}
}
if (isMember && c > 0) {
c = c - 2;
}
return c;
}
It works, but you have to read every line to learn that. What is c? Why 50? Why 2? Is that final c > 0 check needed?
You must never refactor blind. Before touching the code, write down a few examples of what it should return, each with the exact answer you expect. These are tests: tiny questions with known answers that you can re-ask in a second. If all of them still pass after your edit, you have good evidence (not a proof) that you changed nothing important. If one fails, you know right away, while the change is still small.
Good tests include the edges, the places where behaviour switches. Here that means a parcel of exactly 1 kg, an order of exactly 50, and one just under 50. Most mistakes hide at the edges.
The box below runs real JavaScript, right here in your browser, against eight tests. Press the buttons to load each stage of the clean-up, or edit the code yourself and press Run tests. The orange button loads a tempting “tidy-up” that is secretly a mistake.
new Function, with no network. Loops are switched off in this demo, but this function does not need any.Each stage of the clean-up is small and does one thing. Naming the numbers cannot change the result, because FREE_SHIPPING_FROM holds exactly the value 50. Flattening the if statements is a bigger change, and that is where the tests earn their keep. Notice that the stage-two version dropped the c > 0 check. That was safe only because the early return 0 already handles every free order, and every paid rate is above 2. The tests are what let you check that claim in a second instead of arguing about it.
Now try the risky tidy-up. Changing >= to > looks harmless, and it compiles and runs without any error. But an order of exactly 50 stops shipping free. Only the two tests that sit right on that edge notice, which is why edge tests matter. A bug like this could sit in a shop for months before a customer complained.
Change one thing at a time. If you rename, flatten and extract all at once and a test fails, you will not know which change caused it. Run the tests after every step. Do not mix refactoring with new features. First make the code easy to change with no change in behaviour, and only then add the new rule. If something breaks, you will know it was the new rule.
And what if you have no tests? Then writing them is step zero. Even four or five examples taken from the code’s current behaviour will protect you. Real projects usually have a test runner that re-runs hundreds of such checks automatically, but the idea is the same as the table above.
Code is read far more often than it is written, by teammates and by your future self. Readable code makes the next change cheaper and the next bug easier to see. Refactoring is how you pay that cost down in small, safe pieces, instead of in one frightening rewrite. Next time you open a function and wince, write a few tests first, then tidy in small steps.