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

  1. Не пиши код без падающего теста
  2. Не пиши больше одного падающего теста за раз
  3. Не пиши больше кода, чем нужно для прохождения теста

Практический пример: 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
// - Отрицательные числа
// - Очень большие значения
// - Спецсимволы в строках

Практика

  1. Реализуй функцию romanToInt(str) (римские числа → арабские) по TDD: начни с "I" → 1, потом "V" → 5, и так далее
  2. Напиши калькулятор строковых выражений по TDD: "2+3" → 5, "10-4" → 6
  3. Реализуй функцию validatePassword(pwd) по TDD: минимум 8 символов, заглавная буква, цифра
  4. Попробуй BDD-стиль: опиши через Given-When-Then поведение стека (push, pop, peek, isEmpty)

Связанные темы

Ресурсы