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.
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.
1 ┌──────────────────────────────────────┐
2 │ │
3 │ 🔴 RED │
4 │ Napisz test, który nie przechodzi │
5 │ │
6 └───────────────┬──────────────────────┘
7 │
8 ▼
9 ┌──────────────────────────────────────┐
10 │ │
11 │ 🟢 GREEN │
12 │ Napisz minimalny kod do │
13 │ zaliczenia testu │
14 │ │
15 └───────────────┬──────────────────────┘
16 │
17 ▼
18 ┌──────────────────────────────────────┐
19 │ │
20 │ 🔵 REFACTOR │
21 │ Ulepsz kod zachowując │
22 │ przechodzące testy │
23 │ │
24 └───────────────┬──────────────────────┘
25 │
26 └──────────► Powtórz cyklWyobraź sobie, że budujesz system monitorowania dinozaurów. Zacznijmy od 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});Ten test teraz FAILUJE - to dobrze! Nie mamy jeszcze klasy Dinosaur.
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Ą!
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}Teraz zbudujemy bardziej złożony system używając 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// Z TDD masz pewność - jeśli testy przechodzą, kod działa!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});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);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});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 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"!