Перейти к содержимому

3. TDD: разработка через тесты

TDD

TDD (Test-Driven Development) — методология, где тест пишется до реализации.

Цикл Red → Green → Refactor:
🔴 RED → Написать тест, который падает
🟢 GREEN → Написать минимальный код, чтобы тест прошёл
🔵 REFACTOR → Улучшить код, не меняя поведение
slugify.test.js
import { slugify } from './slugify';
describe('slugify', () => {
test('converts spaces to hyphens', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
});
// ❌ FAIL: Cannot find module './slugify'
slugify.js
export function slugify(str) {
return str.toLowerCase().replace(/\s+/g, '-');
}
// ✅ PASS
describe('slugify', () => {
test('converts spaces to hyphens', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('removes special characters', () => {
expect(slugify('Hello, World!')).toBe('hello-world');
});
test('handles multiple spaces', () => {
expect(slugify('too many spaces')).toBe('too-many-spaces');
});
test('handles Russian text', () => {
expect(slugify('Привет мир')).toBe('privet-mir');
});
test('trims hyphens from edges', () => {
expect(slugify(' Hello ')).toBe('hello');
});
});
// Улучшенная реализация
import { transliterate } from 'transliteration';
export function slugify(str) {
return transliterate(str)
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.trim()
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
cart.test.js
describe('ShoppingCart', () => {
let cart;
beforeEach(() => { cart = new ShoppingCart(); });
// Тест 1
test('starts empty', () => {
expect(cart.items).toEqual([]);
expect(cart.total).toBe(0);
});
// Тест 2
test('adds item', () => {
cart.addItem({ id: 1, name: 'Apple', price: 1.5, qty: 2 });
expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(3.0);
});
// Тест 3
test('increases qty for existing item', () => {
cart.addItem({ id: 1, name: 'Apple', price: 1.5, qty: 1 });
cart.addItem({ id: 1, name: 'Apple', price: 1.5, qty: 2 });
expect(cart.items).toHaveLength(1);
expect(cart.items[0].qty).toBe(3);
});
// Тест 4
test('removes item', () => {
cart.addItem({ id: 1, name: 'Apple', price: 1.5, qty: 1 });
cart.removeItem(1);
expect(cart.items).toHaveLength(0);
});
// Тест 5
test('applies discount', () => {
cart.addItem({ id: 1, name: 'Apple', price: 100, qty: 1 });
cart.applyDiscount(10); // 10%
expect(cart.total).toBe(90);
});
});
// cart.js — реализация по мере добавления тестов
export class ShoppingCart {
constructor() {
this.items = [];
this._discount = 0;
}
get total() {
const subtotal = this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 - this._discount / 100);
}
addItem(item) {
const existing = this.items.find(i => i.id === item.id);
if (existing) {
existing.qty += item.qty;
} else {
this.items.push({ ...item });
}
}
removeItem(id) {
this.items = this.items.filter(i => i.id !== id);
}
applyDiscount(percent) {
this._discount = percent;
}
}

Плюсы:

  • Код покрыт тестами с первого дня
  • Продумываешь API до написания кода
  • Меньше отладки, больше уверенности
  • Тесты — живая документация

Минусы:

  • Поначалу медленнее
  • Требует дисциплины
  • Сложнее для UI и интеграций
  • Не всегда уместен (прототипы, R&D)

✅ Бизнес-логика (корзина, расчёты, валидация)
✅ Алгоритмы и трансформации данных
✅ Публичное API библиотек
❌ Прототипы и эксперименты
❌ Простые CRUD без логики
❌ Настройка инфраструктуры

  1. Реализуй функцию validatePassword через TDD (длина, спецсимволы, цифры)
  2. Создай класс EventEmitter через TDD (on, off, emit)
  3. Применить TDD к функции paginate(items, page, perPage)
  • TDD: тест → реализация → рефакторинг
  • Красный → Зелёный → Рефакторинг
  • Хорошо подходит для бизнес-логики
  • Тест описывает желаемое поведение до кода