We use cookies to enhance your experience on the site
CodeWorlds

Test-Driven Development (TDD) - Code Evolution Through Tests

Welcome to the world of Test-Driven Development! Just as scientists in Jurassic Park had to first understand dinosaur DNA before they could clone them, TDD requires us to first define expected behavior (test) before writing the implementation code.

What is TDD?

Test-Driven Development is a programming technique where we first write a test (which initially fails), then write the minimal code needed to pass the test, and finally refactor the code while keeping the tests passing.

The Red-Green-Refactor Cycle

1    +--------------------------------------+
2    |                                      |
3    |   RED                                |
4    |   Write a test that fails            |
5    |                                      |
6    +---------------+----------------------+
7                    |
8                    v
9    +--------------------------------------+
10    |                                      |
11    |   GREEN                              |
12    |   Write minimal code to              |
13    |   pass the test                      |
14    |                                      |
15    +---------------+----------------------+
16                    |
17                    v
18    +--------------------------------------+
19    |                                      |
20    |   REFACTOR                           |
21    |   Improve code while keeping         |
22    |   tests passing                      |
23    |                                      |
24    +---------------+----------------------+
25                    |
26                    +----------> Repeat the cycle

TDD in Practice - Jurassic Park Example

Imagine you are building a dinosaur monitoring system. Let's start with TDD:

Step 1: RED - Write the Test

1// dinosaur.test.js
2import { describe, it, expect } from 'vitest';
3import { Dinosaur } from './dinosaur';
4
5describe('Dinosaur', () => {
6  describe('constructor', () => {
7    it('should create a dinosaur with name and species', () => {
8      // Arrange & Act
9      const dino = new Dinosaur('Rex', 'Tyrannosaurus');
10
11      // Assert
12      expect(dino.name).toBe('Rex');
13      expect(dino.species).toBe('Tyrannosaurus');
14    });
15  });
16
17  describe('isDangerous', () => {
18    it('should return true for carnivore dinosaurs', () => {
19      const dino = new Dinosaur('Rex', 'Tyrannosaurus', 'carnivore');
20      expect(dino.isDangerous()).toBe(true);
21    });
22
23    it('should return false for herbivore dinosaurs', () => {
24      const dino = new Dinosaur('Steggy', 'Stegosaurus', 'herbivore');
25      expect(dino.isDangerous()).toBe(false);
26    });
27  });
28});

This test now FAILS - that is good! We don't have the Dinosaur class yet.

Step 2: GREEN - Minimal Code

1// dinosaur.js
2export class Dinosaur {
3  constructor(name, species, diet = 'unknown') {
4    this.name = name;
5    this.species = species;
6    this.diet = diet;
7  }
8
9  isDangerous() {
10    return this.diet === 'carnivore';
11  }
12}

Now the tests PASS!

Step 3: REFACTOR - Improve the Code

1// dinosaur.js - after refactoring
2const DANGEROUS_DIETS = ['carnivore', 'omnivore'];
3
4export class Dinosaur {
5  #name;
6  #species;
7  #diet;
8
9  constructor(name, species, diet = 'unknown') {
10    this.#validateInput(name, species);
11    this.#name = name;
12    this.#species = species;
13    this.#diet = diet;
14  }
15
16  get name() { return this.#name; }
17  get species() { return this.#species; }
18  get diet() { return this.#diet; }
19
20  #validateInput(name, species) {
21    if (!name || !species) {
22      throw new Error('Name and species are required');
23    }
24  }
25
26  isDangerous() {
27    return DANGEROUS_DIETS.includes(this.#diet);
28  }
29}

Advanced TDD - Park Security System

Now let's build a more complex system using TDD:

Test Suite for SecuritySystem

1// security-system.test.js
2import { describe, it, expect, beforeEach, vi } from 'vitest';
3import { SecuritySystem } from './security-system';
4import { Enclosure } from './enclosure';
5import { Dinosaur } from './dinosaur';
6
7describe('SecuritySystem', () => {
8  let securitySystem;
9  let mockEnclosure;
10  let mockDinosaur;
11
12  beforeEach(() => {
13    mockDinosaur = new Dinosaur('Rex', 'Tyrannosaurus', 'carnivore');
14    mockEnclosure = new Enclosure('Paddock 1', [mockDinosaur], {
15      fenceVoltage: 10000,
16      isActive: true
17    });
18    securitySystem = new SecuritySystem();
19    securitySystem.addEnclosure(mockEnclosure);
20  });
21
22  describe('addEnclosure', () => {
23    it('should add an enclosure to monitoring', () => {
24      const newEnclosure = new Enclosure('Paddock 2', [], { fenceVoltage: 5000 });
25      securitySystem.addEnclosure(newEnclosure);
26
27      expect(securitySystem.getEnclosures()).toHaveLength(2);
28    });
29
30    it('should throw error for duplicate enclosure names', () => {
31      const duplicate = new Enclosure('Paddock 1', [], { fenceVoltage: 5000 });
32
33      expect(() => securitySystem.addEnclosure(duplicate))
34        .toThrow('Enclosure with this name already exists');
35    });
36  });
37
38  describe('checkStatus', () => {
39    it('should return SAFE when all fences are active', () => {
40      expect(securitySystem.checkStatus()).toBe('SAFE');
41    });
42
43    it('should return DANGER when any fence is inactive', () => {
44      mockEnclosure.deactivateFence();
45
46      expect(securitySystem.checkStatus()).toBe('DANGER');
47    });
48
49    it('should return CRITICAL when dangerous dino escapes', () => {
50      mockEnclosure.deactivateFence();
51      mockEnclosure.markEscaped(mockDinosaur);
52
53      expect(securitySystem.checkStatus()).toBe('CRITICAL');
54    });
55  });
56
57  describe('getAlerts', () => {
58    it('should return empty array when everything is safe', () => {
59      expect(securitySystem.getAlerts()).toEqual([]);
60    });
61
62    it('should generate alert when fence voltage drops', () => {
63      mockEnclosure.setFenceVoltage(1000);
64
65      const alerts = securitySystem.getAlerts();
66
67      expect(alerts).toHaveLength(1);
68      expect(alerts[0]).toMatchObject({
69        type: 'LOW_VOLTAGE',
70        enclosure: 'Paddock 1',
71        severity: 'WARNING'
72      });
73    });
74  });
75
76  describe('emergencyProtocol', () => {
77    it('should trigger alarms for all affected zones', async () => {
78      const alarmSpy = vi.fn();
79      securitySystem.onAlarm(alarmSpy);
80
81      await securitySystem.emergencyProtocol('Paddock 1');
82
83      expect(alarmSpy).toHaveBeenCalledWith({
84        type: 'EMERGENCY',
85        enclosure: 'Paddock 1',
86        actions: ['LOCKDOWN', 'EVACUATE', 'ALERT_SECURITY']
87      });
88    });
89  });
90});

Implementation After TDD

1// security-system.js
2export class SecuritySystem {
3  #enclosures = new Map();
4  #alarmCallbacks = [];
5
6  addEnclosure(enclosure) {
7    if (this.#enclosures.has(enclosure.name)) {
8      throw new Error('Enclosure with this name already exists');
9    }
10    this.#enclosures.set(enclosure.name, enclosure);
11  }
12
13  getEnclosures() {
14    return Array.from(this.#enclosures.values());
15  }
16
17  checkStatus() {
18    const enclosures = this.getEnclosures();
19
20    for (const enclosure of enclosures) {
21      const escaped = enclosure.getEscapedDinosaurs();
22      if (escaped.some(dino => dino.isDangerous())) {
23        return 'CRITICAL';
24      }
25    }
26
27    for (const enclosure of enclosures) {
28      if (!enclosure.isFenceActive()) {
29        return 'DANGER';
30      }
31    }
32
33    return 'SAFE';
34  }
35
36  getAlerts() {
37    const alerts = [];
38    const MINIMUM_SAFE_VOLTAGE = 5000;
39
40    for (const enclosure of this.getEnclosures()) {
41      if (enclosure.getFenceVoltage() < MINIMUM_SAFE_VOLTAGE) {
42        alerts.push({
43          type: 'LOW_VOLTAGE',
44          enclosure: enclosure.name,
45          severity: 'WARNING',
46          currentVoltage: enclosure.getFenceVoltage()
47        });
48      }
49    }
50
51    return alerts;
52  }
53
54  onAlarm(callback) {
55    this.#alarmCallbacks.push(callback);
56  }
57
58  async emergencyProtocol(enclosureName) {
59    const alarmData = {
60      type: 'EMERGENCY',
61      enclosure: enclosureName,
62      actions: ['LOCKDOWN', 'EVACUATE', 'ALERT_SECURITY']
63    };
64
65    for (const callback of this.#alarmCallbacks) {
66      await callback(alarmData);
67    }
68  }
69}

Benefits of TDD

1. Confidence in Changes

1// With TDD you have confidence - if tests pass, the code works!

2. Documentation in Code

1describe('DinosaurFeeder', () => {
2  it('should feed herbivores plants only', () => {
3    // This test clearly documents expected behavior
4  });
5
6  it('should not feed carnivores to each other', () => {
7    // Business rule captured in a test!
8  });
9});

3. Better Design - TDD Enforces Loose Coupling

1// TDD leads to Dependency Injection
2class GoodParkManager {
3  constructor(database, mailer) {
4    this.database = database;
5    this.mailer = mailer;
6  }
7}
8
9// Easy to test with mocks:
10const mockDb = { query: vi.fn() };
11const manager = new GoodParkManager(mockDb, mockMailer);

TDD Anti-patterns - What to Avoid

1// ANTI-PATTERN: Test too general
2it('should work', () => {
3  expect(doSomething()).toBeTruthy();
4});
5
6// CORRECT: Specific test
7it('should return dinosaur count for specific species', () => {
8  expect(countDinosaurs('Velociraptor')).toBe(8);
9});
10
11// ANTI-PATTERN: Testing implementation instead of behavior
12it('should call database.query', () => {
13  service.getDinosaurs();
14  expect(database.query).toHaveBeenCalled();
15});
16
17// CORRECT: Testing behavior
18it('should return all dinosaurs from paddock', () => {
19  const result = service.getDinosaurs();
20  expect(result).toHaveLength(5);
21});

TDD Tools

Vitest - Modern Framework

1// vitest.config.js
2import { defineConfig } from 'vitest/config';
3
4export default defineConfig({
5  test: {
6    globals: true,
7    environment: 'node',
8    coverage: {
9      provider: 'v8',
10      threshold: { statements: 80, branches: 80, functions: 80, lines: 80 }
11    }
12  }
13});

Jest - Popular Framework

1// jest.config.js
2module.exports = {
3  testEnvironment: 'node',
4  coverageThreshold: {
5    global: { statements: 80, branches: 80, functions: 80, lines: 80 }
6  }
7};

TDD is a powerful technique that changes the way you think about code. Instead of "write first, test later," you adopt the approach of "define expectations first, then implement"!

Go to CodeWorlds