Test-driven development (TDD) flips the usual order: you write a failing test for behavior that doesn't exist yet, then write just enough code to make it pass, then clean up. The cycle has a name — red, green, refactor — and it repeats in small steps, over and over.
Cart that rejects negative quantitiesSay the requirement is: adding an item to a cart with a negative quantity should throw, instead of silently accepting garbage data.
Red — write the test first, against a Cart class that doesn't exist yet:
test("adding an item with a negative quantity throws", () => {
const cart = new Cart();
expect(() => cart.addItem("sku-1", -2)).toThrow("Quantity must be positive");
});
Run it: it fails immediately, because Cart doesn't exist. That's expected — this is the "red" the cycle is named for.
Green — write the least code that makes it pass:
class Cart {
addItem(sku, quantity) {
if (quantity < 0) throw new Error("Quantity must be positive");
}
}
This is deliberately incomplete — it doesn't even store the item yet — but it makes the one test that exists pass, and that's the only job of this step.
Red again — add the next test, for the behavior the previous step skipped:
test("adding a valid item stores it in the cart", () => {
const cart = new Cart();
cart.addItem("sku-1", 2);
expect(cart.items).toEqual([{ sku: "sku-1", quantity: 2 }]);
});
Green again — extend the implementation just enough:
class Cart {
items = [];
addItem(sku, quantity) {
if (quantity < 0) throw new Error("Quantity must be positive");
this.items.push({ sku, quantity });
}
}
Refactor — with two green tests acting as a safety net, this is a safe moment to clean up naming, extract a validation helper, or restructure — and immediately know, from the tests, if a "harmless" cleanup actually broke something.
TDD earns its keep on logic with clear, checkable inputs and outputs — parsers, validation rules, pricing calculations, state machines — where writing the test first forces you to nail down the exact contract (what counts as valid input? what's the expected output for this edge case?) before you've already committed to an implementation that quietly bakes in wrong assumptions. It also leaves you with a real regression suite as a side effect, for free, instead of as a separate chore afterward.
It's applied badly, though, when it turns into a rule followed for its own sake rather than for what it buys you:
The honest takeaway: TDD is a tool for driving out a clear design under a safety net, not a moral obligation to write every test first no matter the situation. Plenty of excellent, well-tested code is written test-after, and plenty of strictly-by-the-book TDD produces a bloated suite of tests nobody can explain the purpose of six months later.