Хватит гадать: почему TDD — это не мазохизм, а суперсила
Представь: ты пишешь код. Всё работает. Ты доволен. Через месяц возвращаешься к проекту, вносишь одно маленькое изменение — и всё ломается. Знакомо? А теперь представь, что у тебя есть страховочная сетка, которая ловит каждую ошибку за секунду. Это и есть TDD в TypeScript — разработка через тесты.
Многие разработчики думают, что TDD — это для перфекционистов или для проектов NASA. Спойлер: это не так. Test Driven Development — это инструмент, который экономит часы дебага, повышает качество кода и делает рефакторинг безопасным. В этой статье мы разберём TDD TypeScript на реальных примерах, без воды и абстракций.
Если ты пишешь на TypeScript и хочешь перестать бояться своих же правок — читай дальше. Будет интересно.
Что такое TDD (и почему это не про "написать тесты")
TDD (Test-Driven Development) — это методология, где тесты пишутся до кода. Звучит безумно? Только на первый взгляд. Цикл TDD состоит из трёх шагов:
- Красный (Red) — напиши тест, который гарантированно упадёт, потому что кода ещё нет.
- Зелёный (Green) — напиши минимальный код, чтобы тест прошёл.
- Рефакторинг (Refactor) — улучши код, не меняя его поведения.
И так — по кругу. Каждый новый кусочек функциональности начинается с теста. Это дисциплинирует, заставляет думать о дизайне API заранее и даёт уверенность, что всё работает как часы.
Кстати, юнит тесты TypeScript — это не просто "проверка функций". Это документация, спецификация и страховка в одном флаконе.
Инструменты для TDD в TypeScript: что нужно установить
Для тестирования TypeScript нам понадобится:
- Jest — самый популярный тестовый фреймворк. Работает из коробки с TypeScript.
- ts-jest — пресет для Jest, чтобы он понимал TypeScript без дополнительной настройки.
- @types/jest — типы для автодополнения в IDE.
Установка в одну команду:
npm install --save-dev jest ts-jest @types/jest typescript
Добавь в package.json скрипт для запуска тестов:
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
Теперь всё готово для разработки через тесты TypeScript.
Пишем первый тест по TDD: от красного к зелёному
Представим, что нам нужно написать функцию, которая проверяет, является ли строка палиндромом. По классике TDD — начинаем с теста.
Шаг 1. Красный (Red)
Создаём файл isPalindrome.test.ts:
import { isPalindrome } from './isPalindrome';
describe('isPalindrome', () => {
it('должен возвращать true для строки "racecar"', () => {
expect(isPalindrome('racecar')).toBe(true);
});
it('должен возвращать false для строки "hello"', () => {
expect(isPalindrome('hello')).toBe(false);
});
});
Запускаем тест:
npm test
Видим ошибку: Cannot find module './isPalindrome'. Отлично, красная фаза пройдена. Теперь нам нужно написать код, чтобы тест прошёл.
Шаг 2. Зелёный (Green)
Создаём isPalindrome.ts с минимальной реализацией:
export function isPalindrome(str: string): boolean {
return str === str.split('').reverse().join('');
}
Запускаем тесты снова — зелёный! Оба теста проходят. Но это ещё не всё.
Шаг 3. Рефакторинг (Refactor)
Наша функция работает, но есть нюанс: она не обрабатывает строки с пробелами и разным регистром. Добавим новый тест:
it('должен игнорировать регистр и пробелы', () => {
expect(isPalindrome('A man a plan a canal Panama')).toBe(true);
});
Тест падает. Мы снова в красной фазе. Теперь рефакторим код:
export function isPalindrome(str: string): boolean {
const cleaned = str.toLowerCase().replace(/\s/g, '');
return cleaned === cleaned.split('').reverse().join('');
}
Тесты проходят. Мы улучшили код, не сломав старую функциональность. Вот она, магия TDD в TypeScript.
Продвинутые техники: mock, stub и Dependency Injection
В реальных проектах редко бывают чистые функции. Часто приходится работать с базами данных, API или файловой системой. Тут на помощь приходят моки.
Допустим, у нас есть сервис, который получает данные из внешнего API:
export class UserService {
constructor(private httpClient: HttpClient) {}
async getUser(id: number): Promise<User> {
const response = await this.httpClient.get(`/users/${id}`);
return response.data;
}
}
Тест с моком:
import { UserService } from './UserService';
describe('UserService', () => {
it('должен возвращать пользователя по ID', async () => {
const mockHttpClient = {
get: jest.fn().mockResolvedValue({ data: { id: 1, name: 'Alice' } })
};
const service = new UserService(mockHttpClient);
const user = await service.getUser(1);
expect(user.name).toBe('Alice');
expect(mockHttpClient.get).toHaveBeenCalledWith('/users/1');
});
});
Обрати внимание: мы не ходим в реальное API. Мы тестируем только логику UserService. Это и есть unit тесты TypeScript в чистом виде.
Почему TDD делает код лучше (спойлер: не только тесты)
Когда ты практикуешь Test Driven Development, ты невольно начинаешь писать более модульный и гибкий код. Почему?
- Слабая связанность — если функцию сложно протестировать, значит, она делает слишком много. TDD заставляет разбивать код на мелкие, независимые модули.
- Чистые функции — тесты проще писать для функций без побочных эффектов. Ты начинаешь избегать глобальных состояний.
- Документация живёт — тесты — это всегда актуальная документация. Они не врут.
Для TypeScript это особенно актуально, потому что статическая типи