Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Test-Driven Development (TDD) - Ewolucja kodu przez testy

Witaj w świecie Test-Driven Development! Podobnie jak naukowcy w Parku Jurajskim musieli najpierw zrozumieć DNA dinozaurów zanim mogli je sklonować, tak TDD wymaga od nas najpierw zdefiniowania oczekiwanego zachowania (test) zanim napiszemy kod implementacji.

Czym jest TDD?

Test-Driven Development to technika programowania, w której najpierw piszemy test (który początkowo nie przechodzi), następnie piszemy minimalny kod potrzebny do zaliczenia testu, a na końcu refaktoryzujemy kod zachowując przechodzące testy.

Cykl Red-Green-Refactor

1    ┌──────────────────────────────────────┐
2    │                                      │
3    │   🔴 RED                             │
4    │   Napisz test, który nie przechodzi  │
5    │                                      │
6    └───────────────┬──────────────────────┘
789    ┌──────────────────────────────────────┐
10    │                                      │
11    │   🟢 GREEN                           │
12    │   Napisz minimalny kod do           │
13    │   zaliczenia testu                   │
14    │                                      │
15    └───────────────┬──────────────────────┘
161718    ┌──────────────────────────────────────┐
19    │                                      │
20    │   🔵 REFACTOR                        │
21    │   Ulepsz kod zachowując             │
22    │   przechodzące testy                 │
23    │                                      │
24    └───────────────┬──────────────────────┘
2526                    └──────────► Powtórz cykl

TDD w praktyce - przykład z Parku Jurajskiego

Wyobraź sobie, że budujesz system monitorowania dinozaurów. Zacznijmy od TDD:

Krok 1: 🔴 RED - Piszemy 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});

Ten test teraz FAILUJE - to dobrze! Nie mamy jeszcze klasy Dinosaur.

Krok 2: 🟢 GREEN - Minimalny kod

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}

Teraz testy PRZECHODZĄ!

Krok 3: 🔵 REFACTOR - Ulepszamy kod

1// dinosaur.js - po refaktoryzacji
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}

Zaawansowane TDD - System Bezpieczeństwa Parku

Teraz zbudujemy bardziej złożony system używając TDD:

Test Suite dla 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});

Implementacja po 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}

Korzyści z TDD

1. Pewność zmian

1// Z TDD masz pewność - jeśli testy przechodzą, kod działa!

2. Dokumentacja w kodzie

1describe('DinosaurFeeder', () => {
2  it('should feed herbivores plants only', () => {
3    // Ten test jasno dokumentuje oczekiwane zachowanie
4  });
5
6  it('should not feed carnivores to each other', () => {
7    // Reguła biznesowa zapisana w teście!
8  });
9});

3. Lepszy design - TDD wymusza loose coupling

1// TDD prowadzi do Dependency Injection
2class GoodParkManager {
3  constructor(database, mailer) {
4    this.database = database;
5    this.mailer = mailer;
6  }
7}
8
9// Łatwo testować z mockami:
10const mockDb = { query: vi.fn() };
11const manager = new GoodParkManager(mockDb, mockMailer);

TDD Anti-patterns - czego unikać

1// ❌ ANTI-PATTERN: Test zbyt ogólny
2it('should work', () => {
3  expect(doSomething()).toBeTruthy();
4});
5
6// ✅ POPRAWNIE: Test konkretny
7it('should return dinosaur count for specific species', () => {
8  expect(countDinosaurs('Velociraptor')).toBe(8);
9});
10
11// ❌ ANTI-PATTERN: Testowanie implementacji zamiast zachowania
12it('should call database.query', () => {
13  service.getDinosaurs();
14  expect(database.query).toHaveBeenCalled();
15});
16
17// ✅ POPRAWNIE: Testowanie zachowania
18it('should return all dinosaurs from paddock', () => {
19  const result = service.getDinosaurs();
20  expect(result).toHaveLength(5);
21});

Narzędzia do TDD

Vitest - nowoczesny 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 - popularny 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 to potężna technika, która zmienia sposób myślenia o kodzie. Zamiast "najpierw pisz, potem testuj", przyjmujesz podejście "najpierw zdefiniuj oczekiwania, potem implementuj"!

Przejdź do CodeWorlds