Skip to content

Test-Driven Development — TDD Workflow Explained (2026)

DodaTech Updated 2026-06-20 6 min read

In this tutorial, you'll learn about Test. We cover key concepts, practical examples, and best practices.

Test-driven development (TDD) is a software development practice where you write the test before the implementation code, following a strict red-green-refactor cycle that ensures every line of code has a purpose.

What You'll Learn

You'll master the red-green-refactor cycle, work through a real TDD walkthrough building FizzBuzz, understand the London vs Detroit schools of TDD, learn when TDD works best, and recognize common adoption challenges teams face.

Why TDD Matters

TDD shifts testing from an afterthought to a design tool. Writing the test first forces you to think about the API before the implementation — producing cleaner interfaces, better separation of concerns, and near-zero bug escape rates. At DodaTech, Doda Browser's rendering engine was built test-first, catching layout calculation errors before they ever rendered.

TDD Learning Path

flowchart LR
  A[Unit Testing Guide] --> B[TDD Workflow]
  B --> C[Test Pyramid]
  B --> D[CI/CD Testing Pipeline]
  style B fill:#f90,color:#fff

Red-Green-Refactor Cycle

The TDD cycle has three phases:

  1. Red: Write a failing test
  2. Green: Write the minimum code to pass
  3. Refactor: Clean up while keeping tests green

Each cycle takes 1-5 minutes. This keeps you focused and ensures every line of code has a reason to exist.

Real TDD Walkthrough: FizzBuzz

Let's build FizzBuzz with TDD. The rules: return "Fizz" for multiples of 3, "Buzz" for multiples of 5, "FizzBuzz" for multiples of both, and the number as a string otherwise.

Step 1: Red — Write the First Failing Test

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

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

Run the test — it fails because fizzbuzz doesn't exist yet.

 FAIL  ./fizzbuzz.test.js
  ✕ returns "Fizz" for multiples of 3
  ReferenceError: fizzbuzz is not defined

Step 2: Green — Write Minimum Code

// fizzbuzz.js
function fizzbuzz(n) {
  return 'Fizz';
}

module.exports = fizzbuzz;

Run the test — it passes.

Step 3: Add Next Test

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

Red again. Now generalize:

function fizzbuzz(n) {
  if (n % 3 === 0 && n % 5 === 0) return 'FizzBuzz';
  if (n % 3 === 0) return 'Fizz';
  if (n % 5 === 0) return 'Buzz';
  return String(n);
}

Step 4: Complete the Suite

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

test('returns the number as string otherwise', () => {
  expect(fizzbuzz(2)).toBe('2');
});

test('works for 0', () => {
  expect(fizzbuzz(0)).toBe('FizzBuzz');
});

Expected output:

PASS  ./fizzbuzz.test.js
  ✓ returns "Fizz" for multiples of 3 (1 ms)
  ✓ returns "Buzz" for multiples of 5 (1 ms)
  ✓ returns "FizzBuzz" for multiples of 3 and 5 (1 ms)
  ✓ returns the number as string otherwise (1 ms)
  ✓ works for 0 (1 ms)

London vs Detroit Schools

Two schools of thought in TDD:

Aspect London School Detroit School
Focus Behavior of a single object State of the overall system
Mocks Heavy mocking of all collaborators Mocks only at external boundaries
Test style Interaction-based State-based
Suitable for Complex object interactions Data-centric applications

Both are valid. The London school works well for service layers with many collaborators. The Detroit school suits data pipelines and pure computations.

When TDD Works Best

TDD excels in these scenarios:

  • New development: Greenfield projects where the API is still being designed
  • Bug fixes: Write a test that reproduces the bug, then fix until it passes
  • Complex logic: Algorithms, validation rules, and business logic benefit from test-first thinking
  • Refactoring: A test suite gives you the safety net to restructure code confidently

Baby Steps

Take tiny steps when the problem is hard:

// Baby step: start with the simplest case
test('returns "1" when given 1', () => {
  expect(fizzbuzz(1)).toBe('1');
});

// Then add complexity one test at a time
test('returns "2" when given 2', () => {
  expect(fizzbuzz(2)).toBe('2');
});

If a test stays red for more than 5 minutes, you're taking too big a step. Revert and take a smaller one.

Common Adoption Challenges

1. "It Slows Me Down"

TDD feels slower at first. After 2-3 weeks of practice, most developers are faster with TDD than without — fewer debugging sessions, less rework.

2. "We Don't Have Time to Write Tests"

Skipping tests today creates "bug debt" that costs more later. A bug in production costs 10x more to fix than during development.

3. "What Should I Test First?"

Start with the happy path — the simplest case that succeeds. Then add edge cases, error conditions, and boundary values.

4. "My Tests Are Brittle"

Brittle tests usually test implementation details. Focus on behavior, not internal state.

5. "TDD Doesn't Work for Legacy Code"

True — TDD is hard to apply to untested legacy code. Use characterization tests instead: write tests that capture current behavior, then refactor with safety.

Practice Questions

1. What are the three phases of the TDD cycle? Red (write failing test), Green (write minimum code to pass), Refactor (clean up while keeping tests green).

2. What is the difference between London and Detroit TDD? London school mocks all collaborators and tests interactions. Detroit school tests state with minimal mocking.

3. What do you do if a test stays red for more than 5 minutes? Revert and take a smaller step. The test is too big.

4. How do you apply TDD to legacy code? Write characterization tests that capture current behavior before refactoring.

5. Challenge: Build a shopping cart using TDD. Start with adding items, then calculate total, apply discounts, remove items, and handle empty cart.

Mini Project: TDD Calculator

// calculator.js
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

module.exports = { add, subtract, multiply, divide };

Build this with TDD — one operation at a time, red-green-refactor for each.

FAQ

What is the difference between TDD and BDD?

TDD focuses on developer tests driving code design. BDD extends TDD with natural language scenarios (Given/When/Then) readable by non-technical stakeholders.

Do professional developers really use TDD?

Yes. Many top engineering teams practice TDD for critical code paths. Adoption varies by company, but the practice is well-established in the industry.

Can you do TDD without a testing framework?

Technically yes, but frameworks provide assertions, test runners, and reporting. Jest, pytest, and JUnit are the standard choices.

Does TDD guarantee bug-free code?

No. TDD reduces bugs significantly but doesn't eliminate them. Requirements misunderstandings and integration issues still need other testing approaches.

How long does it take to learn TDD?

Most developers feel comfortable after 2-3 weeks of daily practice. The key is consistency — do it on real code, not just tutorials.

What's Next

Test Pyramid — Unit, Integration, E2E Testing Strategy
CI/CD Testing Pipeline — Automating Tests in CI
Unit Testing Guide — Best Practices

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro