Почему 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 вы заметите изменения:
- Код становится более модульным. Вы не сможете написать монолит, если пишете тесты — он просто не будет тестироваться.
- Вы перестанете бояться рефакторинга. Тесты