TDD в JavaScript: как перестать бояться багов и начать кодить с уверенностью

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему TDD — это не модный тренд, а ваш новый лучший друг

Представьте: вы пишете код, всё работает идеально. Вы запускаете приложение — багов нет. Через неделю вы добавляете новую фичу, и... всё ломается. Знакомо? Если вы когда-нибудь тратили часы на поиск ошибки, которая появилась из-за случайного изменения в соседнем модуле — эта статья для вас.

Разработка через тестирование (Test-Driven Development, TDD) — это не просто методология, а настоящий спасательный круг для разработчика. В мире JavaScript, где код меняется быстрее, чем погода, TDD помогает оставаться на плаву. Давайте разберёмся, как это работает, и почему после TDD вы уже не сможете писать код по-старому.

Что такое TDD на самом деле? (Спойлер: это не про тесты)

Многие думают, что TDD — это просто "писать тесты перед кодом". Но на самом деле это философия проектирования. TDD заставляет вас думать о том, как будет использоваться ваш код, до того как вы его напишете. Это как архитектор, который сначала рисует план здания, а потом начинает строить.

Цикл TDD состоит из трёх шагов (часто называют "Red-Green-Refactor"):

  • Red: Напишите тест, который заведомо упадёт (потому что функционала ещё нет).
  • Green: Напишите минимальный код, чтобы тест прошёл.
  • Refactor: Улучшите код, не меняя его поведения (тесты гарантируют, что вы ничего не сломали).

И да, этот цикл повторяется для каждой маленькой единицы функционала. Звучит как замедление? Возможно. Но на дистанции вы сэкономите часы отладки.

Почему JavaScript идеально подходит для TDD

JavaScript — динамический язык. Это его сила и слабость. Ошибки в рантайме — обычное дело, особенно в больших проектах. TDD превращает JavaScript из "гибкого, но хрупкого" инструмента в надёжную платформу.

Кроме того, экосистема JS богата инструментами для тестирования: Jest, Mocha, Chai, Jasmine. Мы будем использовать Jest — он простой, мощный и популярный. Если вы ещё не знакомы с Jest, не волнуйтесь: мы начнём с базы.

Практика: пишем калькулятор с TDD

Теория — это хорошо, но давайте перейдём к реальному коду. Мы напишем простой калькулятор, используя TDD. Это классический пример, но он отлично демонстрирует процесс.

Шаг 1: Настройка проекта

Создайте новую папку и инициализируйте проект:

mkdir tdd-calculator
cd tdd-calculator
npm init -y
npm install --save-dev jest

Добавьте скрипт в package.json:

"scripts": {
  "test": "jest"
}

Шаг 2: Красный — пишем тест для сложения

Создайте файл calculator.test.js:

// calculator.test.js
const { add } = require('./calculator');

describe('Calculator', () => {
  test('adds 2 + 3 to equal 5', () => {
    expect(add(2, 3)).toBe(5);
  });
});

Запустите тест: npm test. Он упадёт с ошибкой, потому что функции add не существует. Это нормально — это красная фаза.

Шаг 3: Зелёный — пишем минимальный код

Создайте calculator.js:

// calculator.js
function add(a, b) {
  return a + b;
}

module.exports = { add };

Запустите тест снова. Зелёный? Отлично! Мы только что прошли зелёную фазу.

Шаг 4: Рефакторинг

Код простой, рефакторить нечего. Но представьте, что мы хотим добавить проверку типов. TDD позволяет делать это безопасно: сначала тест, потом код.

// calculator.test.js
test('throws error if arguments are not numbers', () => {
  expect(() => add('2', 3)).toThrow('Arguments must be numbers');
});

Теперь модифицируем add:

function add(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('Arguments must be numbers');
  }
  return a + b;
}

Тест проходит. Мы только что улучшили код, не сломав ничего.

Реальный сценарий: тестирование асинхронного кода

В веб-разработке мы часто работаем с API. TDD отлично подходит и для асинхронного кода. Допустим, у нас есть функция, которая получает данные пользователя:

// userService.js
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error('User not found');
  return response.json();
}

Как протестировать это без реального API? Используем моки (mocks):

// userService.test.js
const { getUser } = require('./userService');

global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: 'John' }),
  })
);

test('getUser returns user data', async () => {
  const user = await getUser(1);
  expect(user).toEqual({ id: 1, name: 'John' });
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});

Этот тест не зависит от сети, он быстрый и надёжный. TDD здесь помогает спроектировать API вызова до его реализации.

Распространённые ошибки новичков в TDD

Даже зная теорию, легко наступить на грабли. Вот что чаще всего идёт не так:

  • Слишком большие тесты. Тест должен проверять одну вещь. Если тест падает, вы должны сразу знать, что сломалось.
  • Забывать про рефакторинг. Зелёная фаза — не конец. Если код "грязный", тесты не помогут.
  • Тестировать фреймворки. Не надо тестировать, что Array.map работает. Тестируйте свою логику.
  • Игнорировать краевые случаи. Что будет, если передать null? А пустой массив? TDD заставляет думать об этом заранее.

Как TDD меняет ваш код (и вашу жизнь)

После нескольких недель практики TDD вы заметите изменения:

  • Код становится более модульным. Вы не сможете написать монолит, если пишете тесты — он просто не будет тестироваться.
  • Вы перестанете бояться рефакторинга. Тесты

Блоги

Рекомендаций по книгам