Skip to content
Test-Driven Development: Complete TDD Guide with Examples

Test-Driven Development: Complete TDD Guide with Examples

DodaTech Updated Jun 20, 2026 7 min read

Test-Driven Development (TDD) is a software development practice where you write automated tests before writing the production code, cycling through Red (failing test), Green (passing test), and Refactor (clean up) phases for every small piece of functionality.

What You’ll Learn

  • The Red-Green-Refactor cycle and how to apply it
  • Why TDD leads to better design and fewer bugs
  • Real-world TDD examples in Python and JavaScript
  • Common TDD pitfalls and how to avoid them
  • How to introduce TDD to an existing team

Why TDD Matters

Teams using TDD report 40-80% fewer production defects according to studies from IBM and Microsoft. Beyond bug reduction, TDD fundamentally changes how you design code — you naturally write smaller, more testable, more modular units because you design for testability first. Test-driven code has higher cohesion, lower coupling, and better documentation (the tests themselves serve as executable specifications).

Durga Antivirus Pro uses TDD for all signature parsing modules — when a single missed edge case could let malware through, writing the test first ensures every requirement is explicitly verified.

Learning Path

    flowchart LR
  A[Testing Basics] --> B[Code Smells & Refactoring]
  B --> C[TDD<br/>You are here]
  C --> D[Unit Testing]
  D --> E[CI/CD Pipeline]
  style C fill:#f90,color:#fff
  

The TDD Cycle

TDD follows three repeating steps, often called Red-Green-Refactor:

1. Red — Write a Failing Test

Write a test that defines the behavior you want. It should fail because the feature doesn’t exist yet.

# test_calculator.py
from calculator import add

def test_add_returns_sum():
    result = add(2, 3)
    assert result == 5

Running this test produces a failure:

E   ModuleNotFoundError: No module named 'calculator'

2. Green — Write the Minimum Code

Write just enough production code to make the test pass — nothing more.

# calculator.py
def add(a, b):
    return a + b

Now the test passes:

1 passed in 0.01s

3. Refactor — Clean Up

Improve the code without changing its behavior. Tests stay green throughout.

# calculator.py — add type hints and docstring
def add(a: int, b: int) -> int:
    """Return the sum of two integers."""
    return a + b

Repeat

Each cycle takes 1-5 minutes. You repeat it hundreds of times per feature, building up the solution incrementally.

TDD Example: FizzBuzz

Let’s walk through a complete TDD session for the classic FizzBuzz problem.

First Test: Multiples of 3

# test_fizzbuzz.py
from fizzbuzz import fizzbuzz

def test_returns_fizz_for_multiples_of_3():
    assert fizzbuzz(3) == "Fizz"
    assert fizzbuzz(6) == "Fizz"

Write the minimum code:

# fizzbuzz.py
def fizzbuzz(n):
    return "Fizz"

Second Test: Multiples of 5

def test_returns_buzz_for_multiples_of_5():
    assert fizzbuzz(5) == "Buzz"
    assert fizzbuzz(10) == "Buzz"

Update code:

def fizzbuzz(n):
    if n % 5 == 0:
        return "Buzz"
    return "Fizz"

But this breaks the earlier test — fizzbuzz(3) now returns None. TDD catches this immediately.

Third Test: Multiples of Both 3 and 5

def test_returns_fizzbuzz_for_multiples_of_15():
    assert fizzbuzz(15) == "FizzBuzz"

Full implementation:

def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 5 == 0:
        return "Buzz"
    if n % 3 == 0:
        return "Fizz"
    return str(n)

Fourth Test: Non-Multiples

def test_returns_number_as_string():
    assert fizzbuzz(1) == "1"
    assert fizzbuzz(2) == "2"
    assert fizzbuzz(7) == "7"

All tests pass. We’ve built FizzBuzz with full test coverage, one small step at a time.

Benefits of TDD

Better Design

When you write tests first, you naturally design for testability. This means smaller functions, dependency injection, and clear interfaces. Code written with TDD has measurably lower coupling.

Executable Documentation

Tests are never out of date — if they pass, they accurately describe the system. New team members read tests to understand expected behavior.

Regression Safety Net

Every time you run the test suite, you verify that existing behavior still works. This makes refactoring safe and encourages continuous improvement.

Faster Debugging

When a test fails, you know exactly which behavior broke. Without TDD, you might not discover a regression until staging or production.

Common TDD Mistakes

1. Writing Too Large a Test

Each test should verify one specific behavior. A test that checks five things is hard to debug when it fails.

Fix: One assertion per test. Use descriptive test names.

2. Skipping the Red Phase

Writing a test that already passes means you haven’t verified it tests anything. Always watch it fail first.

Fix: Run the test before writing production code. Confirm it fails.

3. Over-Mocking

Mocking every dependency creates brittle tests that verify mock behavior, not real behavior.

Fix: Mock only external boundaries (network, filesystem, database). Use real objects for internal dependencies.

4. Writing Tests After Code

Writing tests after the fact is testing, not TDD. You lose the design benefits and often end up with untestable code that requires heroic mocking.

Fix: Write the test first. If it’s hard to write, your design needs improvement.

5. Not Refactoring

TDD’s third step is crucial. Without refactoring, you accumulate technical debt even with full test coverage.

Fix: After every green cycle, look for duplication, unclear names, and awkward structures.

6. Abandoning TDD Under Pressure

“Let’s skip tests to ship faster” is tempting. The opposite is true — untested code causes regressions that slow future delivery.

Fix: TDD is fastest in the long run. Short-term speed gains from skipping tests are quickly lost to debugging.

TDD Adoption Strategy

For New Projects

Start with TDD from day one. The first test might be “the app starts” or “the API responds to a health check.” Build the test suite as you build the application.

For Existing Codebases

Introduce TDD incrementally:

  1. Write tests for new features using TDD
  2. Add tests when fixing bugs — write a failing test that reproduces the bug, then fix it
  3. Cover critical paths — authentication, payment, data processing
  4. Refactor legacy code under test coverage

Team Adoption

TDD requires discipline. Start with pair programming — one developer writes the test, the other writes the code. Rotate pairs to spread the practice.

TDD in JavaScript

// fizzbuzz.test.js
const fizzbuzz = require('./fizzbuzz');

test('returns Fizz for multiples of 3', () => {
  expect(fizzbuzz(3)).toBe('Fizz');
  expect(fizzbuzz(6)).toBe('Fizz');
});

test('returns Buzz for multiples of 5', () => {
  expect(fizzbuzz(5)).toBe('Buzz');
  expect(fizzbuzz(10)).toBe('Buzz');
});

test('returns FizzBuzz for multiples of 15', () => {
  expect(fizzbuzz(15)).toBe('FizzBuzz');
});

test('returns number as string for non-multiples', () => {
  expect(fizzbuzz(1)).toBe('1');
  expect(fizzbuzz(7)).toBe('7');
});

Practice Questions

1. What are the three phases of the TDD cycle?

Red (write a failing test), Green (write minimum code to pass), Refactor (clean up while keeping tests green).

2. Why should you watch the test fail before writing implementation?

To confirm the test actually tests something. A test that passes immediately might not be verifying the right behavior.

3. What is the maximum recommended duration for a single TDD cycle?

1–5 minutes. If a cycle takes longer, the unit of work is too large.

4. How does TDD improve code design?

By forcing you to design for testability — smaller functions, dependency injection, clear interfaces, and lower coupling.

5. How do you introduce TDD to an existing codebase?

Start with new features (write tests first), add tests when fixing bugs, cover critical paths, then gradually refactor legacy code under test.

Challenge: Implement a shopping cart using strict TDD. Write tests for addItem, removeItem, applyDiscount, and calculateTotal before writing any production code. Every test must fail before it passes.

FAQ

What is the difference between TDD and unit testing?
TDD is a development process (test first, then code). Unit testing is a verification activity that can happen before or after coding. TDD always uses unit testing, but unit testing can exist without TDD.
Does TDD guarantee bug-free code?
No. TDD reduces bugs significantly but doesn’t eliminate them. Missing requirements, integration issues, and edge cases you didn’t think of can still cause bugs.
Should I use TDD for UI code?
TDD works well for business logic and data processing. For UI, consider testing rendered output rather than implementation details.
How do I TDD when requirements are unclear?
Start with the most certain requirement and write a test for it. The test helps clarify the interface. Iterate as understanding grows.
Can you do TDD without a test framework?
Technically yes, but test frameworks provide assertions, reporting, and test runners that make TDD practical.

What’s Next

TutorialWhat You’ll Learn
Unit Testing Best PracticesWriting effective, maintainable unit tests
Integration Testing StrategiesTesting component interactions
Continuous Testing in CI/CDAutomating TDD in the pipeline

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro