Introduction to Test-Driven Development (TDD)

Table of Contents
TDD (Test-Driven Development)
TDD (Test-Driven Development)
Red Phase
Red Phase
Green Phase
Green Phase
Refactor Phase
Refactor Phase
Introduction to Test-Driven Development (TDD)
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.
The TDD Cycle
| 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.
🔴 Red
🟢 Green
🔵 Refactor
A Concrete Example: Multiplication
The textbook example is small, but working through it slowly is worth it.
Step 1 — Red (Failing Test)
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);
});
Step 2 — Green (Make it Pass)
Write just enough code to satisfy the test. Nothing more. No early optimization.
function multiply(a, b) {
return a * b;
}
Step 3 — Refactor
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:
11 function multiply(a, b) {2+ if (typeof a !== 'number' || typeof b !== 'number') {3+ throw new TypeError('Both arguments must be numbers');4+ }25 return a * b;36 }
Why It Actually Works
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.
The Benefits
- Fewer bugs — Issues are caught instantly, not days later in QA.
- Better design — You define the interface before the implementation. That order matters.
- Fearless refactoring — A solid test suite means you can change code without holding your breath.
- Living documentation — Tests describe exactly how the code behaves. They don't lie.
| 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 |
The mistakes I made at first
- Writing too much code before running the test.
- Skipping the Refactor step entirely (it's not optional).
- Testing implementation details instead of external behavior — those tests break on every refactor.
- Writing tests after the code and calling it TDD. That's just testing.
Where to Start
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 PracticesYou Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Unit Tests in JavaScript: Practical Patterns That Actually Prevent Production Bugs
Pragmatic guide to writing resilient JavaScript unit tests: the AAA pattern, test fixtures, property-based testing with fast-check, and boundary mocking.
Read more
Mastering E2E Testing with Playwright in 2026
Mastering E2E testing with Playwright in 2026: auto-waiting, browser context isolation, network interception, auth storage, and CI parallelization.
Read more
TypeScript Generics: Advanced Patterns for Type-Safe APIs
Master advanced TypeScript generics patterns: conditional types, mapped types, template literal types, branded types, discriminated unions, and building type-safe production libraries.
Read more