TDD (Test-Driven Development)
TDD — методология разработки, при которой тесты пишутся ДО кода. Цикл: Red → Green → Refactor.
Зачем нужно
TDD заставляет продумать интерфейс и поведение функции до написания реализации. Результат: чистый код, минимум лишнего, полное покрытие тестами. Кент Бек (создатель TDD) говорит: «Код без тестов — legacy-код».
Где используется
В бизнес-логике, утилитарных модулях, API-эндпоинтах. Особенно эффективен для алгоритмов, валидации, расчётов. Менее удобен для UI-компонентов и прототипов.
Предпосылки
Основы тестирования, Виды тестов
Цикл Red-Green-Refactor
┌─────────┐
│ RED │ 1. Напиши падающий тест
│ (fail) │
└────┬────┘
│
┌────▼────┐
│ GREEN │ 2. Напиши минимальный код, чтобы тест прошёл
│ (pass) │
└────┬────┘
│
┌────▼────┐
│REFACTOR │ 3. Улучши код, не ломая тесты
│(improve)│
└────┬────┘
│
└──────→ Повторяй
Правила TDD
- Не пиши код без падающего теста
- Не пиши больше одного падающего теста за раз
- Не пиши больше кода, чем нужно для прохождения теста
Практический пример: FizzBuzz по TDD
Шаг 1 — RED: первый тест
// fizzBuzz.test.js
const fizzBuzz = require('./fizzBuzz');
test('возвращает "1" для числа 1', () => {
expect(fizzBuzz(1)).toBe('1');
});
# Запускаем — тест падает (RED)
# ReferenceError: fizzBuzz is not defined
Шаг 2 — GREEN: минимальная реализация
// fizzBuzz.js
function fizzBuzz(n) {
return '1'; // Минимум для прохождения теста!
}
module.exports = fizzBuzz;
# Тест проходит (GREEN) ✓
Шаг 3 — RED: следующий тест
test('возвращает "2" для числа 2', () => {
expect(fizzBuzz(2)).toBe('2');
});
Шаг 4 — GREEN: обобщаем
function fizzBuzz(n) {
return String(n); // Теперь работает для любого числа
}
Шаг 5 — RED: Fizz
test('возвращает "Fizz" для числа 3', () => {
expect(fizzBuzz(3)).toBe('Fizz');
});
Шаг 6 — GREEN
function fizzBuzz(n) {
if (n % 3 === 0) return 'Fizz';
return String(n);
}
Шаг 7 — RED: Buzz
test('возвращает "Buzz" для числа 5', () => {
expect(fizzBuzz(5)).toBe('Buzz');
});
Шаг 8 — GREEN
function fizzBuzz(n) {
if (n % 3 === 0) return 'Fizz';
if (n % 5 === 0) return 'Buzz';
return String(n);
}
Шаг 9 — RED: FizzBuzz
test('возвращает "FizzBuzz" для числа 15', () => {
expect(fizzBuzz(15)).toBe('FizzBuzz');
});
Шаг 10 — GREEN + REFACTOR
function fizzBuzz(n) {
if (n % 15 === 0) return 'FizzBuzz';
if (n % 3 === 0) return 'Fizz';
if (n % 5 === 0) return 'Buzz';
return String(n);
}
Финальный набор тестов
describe('fizzBuzz', () => {
test('обычное число → строка числа', () => {
expect(fizzBuzz(1)).toBe('1');
expect(fizzBuzz(2)).toBe('2');
});
test('кратное 3 → "Fizz"', () => {
expect(fizzBuzz(3)).toBe('Fizz');
expect(fizzBuzz(6)).toBe('Fizz');
});
test('кратное 5 → "Buzz"', () => {
expect(fizzBuzz(5)).toBe('Buzz');
expect(fizzBuzz(10)).toBe('Buzz');
});
test('кратное 15 → "FizzBuzz"', () => {
expect(fizzBuzz(15)).toBe('FizzBuzz');
expect(fizzBuzz(30)).toBe('FizzBuzz');
});
});
TDD vs BDD
| Аспект | TDD | BDD |
|---|---|---|
| Фокус | Техническая корректность | Поведение с точки зрения бизнеса |
| Язык | Технический | Бизнес (Given-When-Then) |
| Кто пишет | Разработчик | Разработчик + аналитик |
| Инструменты | Jest, Vitest | Cucumber, Jest + describe |
Пример BDD-стиля
describe('Корзина покупок', () => {
describe('Когда пользователь добавляет товар', () => {
test('Тогда товар появляется в корзине', () => {
// Given (Допустим)
const cart = new Cart();
const product = { id: 1, name: 'Книга', price: 500 };
// When (Когда)
cart.add(product);
// Then (Тогда)
expect(cart.items).toHaveLength(1);
expect(cart.items[0].name).toBe('Книга');
});
});
describe('Когда пользователь добавляет тот же товар дважды', () => {
test('Тогда количество увеличивается, а не дублируется', () => {
const cart = new Cart();
const product = { id: 1, name: 'Книга', price: 500 };
cart.add(product);
cart.add(product);
expect(cart.items).toHaveLength(1);
expect(cart.items[0].quantity).toBe(2);
});
});
});
Когда TDD работает хорошо
- Чистая бизнес-логика (расчёт скидки, валидация)
- Алгоритмы (сортировка, поиск, парсинг)
- API-эндпоинты (request → response)
- Утилитарные функции
- Фикс багов (сначала тест, воспроизводящий баг → потом фикс)
Когда TDD менее удобен
- UI-компоненты (пока не ясен дизайн)
- Прототипы и исследования
- Код с тяжёлыми внешними зависимостями (БД, файловая система)
- Одноразовые скрипты
Частые ошибки
1. Пропуск этапа Refactor
// После GREEN многие переходят сразу к следующему тесту.
// Refactor — ключевая часть! Убери дублирование, улучши имена.
2. Слишком большие шаги
// ПЛОХО: сразу писать тест на всю функцию целиком
test('fizzBuzz работает правильно для 1-100', () => { /* ... */ });
// ХОРОШО: маленькие шаги, один аспект за раз
test('возвращает число как строку', () => { /* ... */ });
3. Тесты привязаны к реализации
// ПЛОХО: тест ломается при рефакторинге
test('использует switch для выбора', () => { /* ... */ });
// ХОРОШО: тест проверяет результат
test('возвращает "Fizz" для кратных 3', () => { /* ... */ });
4. Игнорирование граничных случаев
// Не забывайте тестировать:
// - Пустые входные данные
// - null, undefined
// - Отрицательные числа
// - Очень большие значения
// - Спецсимволы в строках
Практика
- Реализуй функцию
romanToInt(str)(римские числа → арабские) по TDD: начни с "I" → 1, потом "V" → 5, и так далее - Напиши калькулятор строковых выражений по TDD: "2+3" → 5, "10-4" → 6
- Реализуй функцию
validatePassword(pwd)по TDD: минимум 8 символов, заглавная буква, цифра - Попробуй BDD-стиль: опиши через Given-When-Then поведение стека (push, pop, peek, isEmpty)