TDD: red, green, refactor
Write the test before the code that passes it. A live walkthrough of the red-green-refactor loop, building a slugify function from nothing.

Most people write the code first and the test after, if they write one at all. Test-driven development flips that: you write a failing test for behavior that doesn't exist yet, watch it fail for the right reason, then write just enough code to make it pass. It sounds backwards until you've done it once. Let's build a slugify function this way, from nothing.
The loop
Red means a test exists and fails, ideally because the thing it's testing doesn't exist yet. Green means you wrote the smallest amount of code that makes it pass, not the most elegant amount. Refactor means you clean up, with the test suite as your proof that you didn't change behavior while you did it.
Red: write the test first
We need a function that turns a post title into a URL slug. Before writing slugify itself, write down what it should do:
import { describe, it, expect } from "vitest";
import { slugify } from "./slugify.ts";
describe("slugify", () => {
it("lowercases and hyphenates a title", () => {
expect(slugify("Hello World")).toBe("hello-world");
});
});Run it now and it fails immediately, not because the logic is wrong but because slugify doesn't exist. That's the correct kind of red. If a test passes before you've written the implementation, something's off, either the test isn't checking what you think, or it's testing a no-op.
Green: the smallest thing that passes
Resist the urge to handle every edge case up front. Write just enough:
Green. Ship this exact version and move on to the next test, because that's how the loop works: one small test, one small implementation, repeat. Writing the full, polished slugify in one pass and then reverse-engineering tests for it afterward isn't TDD, it's just testing after the fact with extra steps.
Add the next case
Real titles have punctuation. Add a test for it, and watch it go red before you touch the implementation:
That fails, since the current implementation only handles spaces. Now extend it, the minimum needed:
Both pass, plus a third case that fell out of the fix for free, since collapsing repeated spaces with / +/g handles double spaces without any extra code. That's a common TDD moment: a test you didn't write yet turns out already covered because the fix for another test happened to generalize.
Refactor without fear
The regex chain works but reads a little dense. Split it into named steps:
Same three passes. That's the entire point of the refactor step: the tests are the proof that "cleaner" didn't quietly become "different." Without them, you'd be refactoring on faith.
Quick check
You refactor a function and all its tests still pass. What does that actually tell you?
Is TDD always worth it
Honestly, no. For logic with real edge cases, like parsing, validation, or pricing rules, writing the test first forces you to nail down what "correct" means before you can talk yourself into a shortcut, and that's genuinely valuable. For exploratory UI work, where you don't yet know what the component should look like, writing tests first usually means rewriting them twice as the design changes. I write tests first for the tricky logic and after for the stuff I'm still feeling out. Anyone who tells you it's all-or-nothing hasn't shipped much either way.
Previous: arrange, act, assert. Next: edge cases and the bugs you would have shipped, where slugify-style thinking meets a real security bug from a production codebase.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


