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.
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.
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 cycleImagine you are building a dinosaur monitoring system. Let's start with TDD:
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.
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!
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}Now let's build a more complex system using TDD:
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});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}1// With TDD you have confidence - if tests pass, the code works!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});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);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});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});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"!