
TDD (Test-Driven Development)
TDD (Test-Driven Development)
Red Phase
Red Phase
Green Phase
Green Phase
Refactor Phase
Refactor Phase
For a long time I wrote tests the way most developers do: after the code. I'd finish a feature, feel reasonably good about it, then write tests to confirm it worked the way I thought it should. The tests always passed. That was my first mistake.
One afternoon I refactored a utility function - cleaned it up, made it "better", and broke it in a way I didn't notice for two days. The existing tests were testing my implementation, not the behavior. When I changed the implementation, the tests followed. Nobody caught it until QA.
That's when I actually tried TDD. Not because someone told me to, but because I was tired of that particular failure mode.
| Phase | Action | Mindset | Goal |
|---|---|---|---|
| 🔴 Red | Write failing test | Define expected behavior first | Test describes the API |
| 🟢 Green | Write minimal code to pass | Resist over-engineering | Make the test pass - nothing more |
| 🔵 Refactor | Improve design & remove duplication | Tests are your safety net | Clean code while staying green |
TDD follows a repeating three-step rhythm. Once you've done it a few times it becomes muscle memory, but the first few times it feels backwards.
What is the purpose of the Red phase in TDD?
The textbook example is small, but working through it slowly is worth it.
Write the test first. It fails - multiply doesn't exist yet. That's fine. That's the point.
test('multiplies two numbers', () => {
expect(multiply(3, 4)).toBe(12);
});
Write just enough code to satisfy the test. Nothing more. No early optimization.
function multiply(a, b) {
return a * b;
}
The code is already clean here. But as things grow, this is where you remove duplication, improve naming, and clean up abstractions, while the tests confirm nothing broke.
Here's what refactoring toward correctness looks like once you add type safety:
function multiply(a, b) {if (typeof a !== 'number' || typeof b !== 'number') {throw new TypeError('Both arguments must be numbers');}return a * b;}
After making a test pass (Green phase), what should you do next?
I was skeptical about the design benefits until I tried TDD on a piece of code with unclear requirements. Writing the test first forced me to ask: what should this actually do? I had to define the interface before writing any logic. That turned out to be the harder, and more important - question.
| Without TDD | With TDD | |
|---|---|---|
| Bug Discovery | Found in production or manual testing | Caught instantly during development |
| Design | Implementation drives API design | API designed first (test-first) |
| Refactoring | Risky - no safety net | Confident - tests verify behavior |
| Documentation | Separate docs that get outdated | Tests = always-up-to-date docs |
What is the most common mistake when starting with TDD?
Start small. Pick a utility function - a formatter, a validator, something self-contained. Write the test first, watch it fail, then make it pass. One cycle. See how it feels.
The design benefits don't show up in a three-line function. They show up when you're building something with real complexity and your tests are constantly telling you where the API is confusing or where the logic has too many responsibilities. That's TDD doing its job.
TDD Testing JavaScript Best PracticesClean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Pragmatic guide to writing resilient JavaScript unit tests: the AAA pattern, test fixtures, property-based testing with fast-check, and boundary mocking.
Read more
A comprehensive guide on migrating from Jest to Vitest for modern React applications. Learn how to leverage Vitest's speed and native ESM support alongside React Testing Library to write resilient, maintainable test suites.
Read more
Implementing CI eval gates and distributed tracing for LLM pipelines: automated regression testing, token tracking, and LangSmith dataset benchmarking.
Read more