The Quest Begins (The "Why")
Look, I remember the first time I inherited a codebase that felt like a haunted house. Every change I made would wake up a ghost in some far‑away corner, and I’d spend hours chasing down bugs that seemed to appear out of nowhere. I was constantly firefighting, and the joy of building new features was drowned by the dread of breaking something I couldn’t even see.
One day, after yet another late‑night debugging session where I fixed a null‑pointer only to discover it had masked a deeper logic error, I asked myself: What if I could know, before I even wrote a line of production code, that my change would work? That question led me down the rabbit hole of Test‑Driven Development, and the first practice that truly changed the way I write code was writing a failing test that describes the exact behavior I want, before I write any implementation.
The Revelation (The Insight)
The “aha!” moment came when I realized that a test isn’t just a safety net; it’s a specification written in executable form. When I start with a failing test, I’m forced to articulate the desired outcome in clear, unambiguous terms. The test becomes a contract between me and the future me (or anyone else who touches the code). If I can’t write that test, I probably don’t understand the feature well enough yet.
Why does this matter? Because the act of writing the test first surfaces misunderstandings early. Instead of discovering a flawed assumption after I’ve tangled myself in implementation details, I confront it while the test is still red. The feedback loop tightens from hours or days to minutes.
Think of it like preparing a spell: you first draw the sigil (the test) and only then do you gather the components (the code). If the sigil is wrong, the spell fizzles before you waste any reagents.
Wielding the Power (Code & Examples)
Let’s look at a tiny but real‑world example: a function that calculates the total price of a shopping cart, applying a discount if the cart contains a promo code.
The Struggle (Before)
I used to dive straight into the implementation:
// cart.js – before TDD
function calculateTotal(items, promoCode) {
let sum = 0;
for (const item of items) {
sum += item.price * item.quantity;
}
if (promoCode === 'SAVE10') {
return sum * 0.9;
}
return sum;
}
I shipped it, wrote a few ad‑hoc checks in the console, and moved on. A week later, a teammate added a new promo type that gave a fixed $5 off. They edited the function, missed the edge case where the promo code was an empty string, and suddenly the total became NaN for carts without a promo. The bug slipped into production because we only had manual tests.
The Victory (After)
Now I start with a failing test that captures the exact behavior I want:
// cart.test.js
describe('calculateTotal', () => {
it('returns the sum of item prices when no promo is supplied', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 1 },
];
expect(calculateTotal(items, null)).toBe(25); // ← fails at first
});
it('applies a 10% discount for the SAVE10 promo', () => {
const items = [{ price: 20, quantity: 1 }];
expect(calculateTotal(items, 'SAVE10')).toBe(18); // ← fails at first
});
it('returns the original sum when promoCode is an empty string', () => {
const items = [{ price: 7, quantity: 3 }];
expect(calculateTotal(items, '')).toBe(21); // ← fails at first
});
});
See how each test states a behavior in plain English? I run the test suite – all are red. Then I write the simplest code to make them pass:
function calculateTotal(items, promoCode) {
let sum = 0;
for (const item of items) {
sum += item.price * item.quantity;
}
if (promoCode === 'SAVE10') {
return sum * 0.9;
}
// treat any falsy promo (null, undefined, '') as “no discount”
if (!promoCode) {
return sum;
}
// future promo types can be added here
return sum;
}
All tests turn green. I feel that rush — like finally beating the final boss in Celeste without a checkpoint. The implementation is now guided by
Top comments (0)