Антон Ларичев

Введение
Внедрить тесты в проект, где их никогда не было, — одна из самых частых задач для middle-разработчика. Вы приходите на проект, видите тысячи строк кода без единого теста и задаётесь вопросом: с чего вообще начать тестирование legacy-кода? Попытка покрыть всё разом приводит к выгоранию и заброшенным тестам.
В этой статье разберём пошаговую стратегию внедрения тестов в существующий проект на JavaScript или TypeScript. Без фанатизма, без переписывания всего кода — только практичный подход, который работает в реальных командах.
Почему нельзя просто взять и покрыть всё тестами
Первый порыв — написать тесты на всё сразу. Это ошибка. В проекте без тестов код, как правило, тесно связан: функции зависят друг от друга, модули напрямую обращаются к базе данных, конфигурация вшита в бизнес-логику. Такой код сложно тестировать без рефакторинга.
Вместо этого нужна стратегия тестирования, которая даёт результат с первого дня и постепенно наращивает покрытие.
Шаг 1: настройка инфраструктуры тестирования
Прежде чем писать первый тест, подготовьте окружение. Для JavaScript и TypeScript проектов Vitest — оптимальный выбор: быстрый, совместимый с ESM, минимальная настройка.
Установка и конфигурация Vitest
npm install -D vitest
Добавьте конфигурацию в корень проекта:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
// Не ставьте порог покрытия сразу — он будет расти постепенно
},
},
});
Добавьте скрипты в package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
Настройте CI-пайплайн так, чтобы тесты запускались на каждый пуш. Даже если тестов пока один — пайплайн должен работать с самого начала.
Шаг 2: определить критические зоны для первых тестов
Не пишите тесты на всё подряд. Начните с кода, который ломается чаще всего или несёт наибольшие бизнес-риски.
Как выбрать, что тестировать первым
Проанализируйте проект по трём критериям:
- Частота багов — посмотрите историю коммитов и тикетов. Модули с частыми фиксами — кандидаты номер один.
- Бизнес-критичность — оплата, регистрация, авторизация. Ошибка здесь стоит дороже всего.
- Частота изменений — код, который меняется каждую неделю, нуждается в защите от регрессий.
# Найти файлы с наибольшим количеством коммитов
git log --pretty=format: --name-only | sort | uniq -c | sort -rn | head -20
Эта команда покажет файлы, которые менялись чаще всего — именно с них стоит начать покрытие тестами.
Шаг 3: написать первые unit-тесты для чистых функций
В любом проекте есть функции, которые принимают данные и возвращают результат без побочных эффектов. Валидаторы, форматтеры, калькуляторы, парсеры — это идеальные кандидаты для первых unit-тестов.
// src/utils/price.ts
export function formatPrice(amount: number, currency: string = 'RUB'): string {
if (amount < 0) throw new Error('Сумма не может быть отрицательной');
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency,
}).format(amount);
}
export function calculateDiscount(price: number, percent: number): number {
if (percent < 0 || percent > 100) {
throw new Error('Процент скидки должен быть от 0 до 100');
}
return Math.round(price * (1 - percent / 100));
}
Тесты для этих функций:
// src/utils/price.test.ts
import { describe, it, expect } from 'vitest';
import { formatPrice, calculateDiscount } from './price';
describe('formatPrice', () => {
it('форматирует цену в рублях', () => {
const result = formatPrice(1500);
expect(result).toContain('1');
expect(result).toContain('500');
});
it('выбрасывает ошибку для отрицательной суммы', () => {
expect(() => formatPrice(-100)).toThrow('Сумма не может быть отрицательной');
});
});
describe('calculateDiscount', () => {
it('рассчитывает скидку 20%', () => {
expect(calculateDiscount(1000, 20)).toBe(800);
});
it('возвращает полную цену при скидке 0%', () => {
expect(calculateDiscount(500, 0)).toBe(500);
});
it('отклоняет невалидный процент', () => {
expect(() => calculateDiscount(100, 150)).toThrow();
});
});
Такие тесты пишутся за минуты, не требуют рефакторинга основного кода и сразу дают уверенность в корректности.
Шаг 4: добавить интеграционные тесты для критичных сценариев
После unit-тестов переходите к интеграционным. Они проверяют взаимодействие модулей — например, что сервис корректно вызывает репозиторий и обрабатывает результат.
Для изоляции внешних зависимостей используйте моки Vitest:
// src/services/order.test.ts
import { describe, it, expect, vi } from 'vitest';
import { OrderService } from './order.service';
import { OrderRepository } from '../repositories/order.repository';
// Мокаем модуль репозитория
vi.mock('../repositories/order.repository');
describe('OrderService', () => {
it('создаёт заказ и возвращает его ID', async () => {
// Настраиваем мок
const mockCreate = vi.fn().mockResolvedValue({ id: 42, status: 'created' });
vi.mocked(OrderRepository).mockImplementation(() => ({
create: mockCreate,
}) as any);
const service = new OrderService(new OrderRepository());
const result = await service.createOrder({ productId: 1, quantity: 2 });
expect(result.id).toBe(42);
expect(mockCreate).toHaveBeenCalledWith({
productId: 1,
quantity: 2,
});
});
});
Шаг 5: внедрить правило — новый код только с тестами
Самый важный шаг в стратегии тестирования. Договоритесь с командой: любой новый код или исправление бага сопровождается тестом. Это правило не требует покрывать старый код, но гарантирует, что покрытие растёт с каждым коммитом.
Добавьте проверку в CI:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:run
Когда тесты блокируют мёрж, команда быстро привыкает писать их.
Шаг 6: постепенно повышать покрытие тестами
После того как инфраструктура работает и новый код пишется с тестами, начинайте наращивать покрытие старого кода. Используйте отчёт coverage для поиска непокрытых участков:
npm run test:coverage
Установите начальный порог покрытия чуть выше текущего уровня и повышайте его каждый спринт:
// vitest.config.ts
coverage: {
thresholds: {
statements: 30, // Начните с реального значения
branches: 25,
functions: 30,
lines: 30,
},
}
Каждые две недели увеличивайте пороги на 3-5%. Через полгода покрытие вырастет до приемлемого уровня без стресса для команды.
Частые ошибки при внедрении тестов в legacy-проект
Попытка покрыть всё за спринт. Это приводит к сотням хрупких тестов, которые ломаются при каждом изменении. Внедряйте тесты постепенно.
Тестирование деталей реализации. Тест, который проверяет порядок вызовов приватных методов, сломается при любом рефакторинге. Тестируйте поведение, а не реализацию.
Игнорирование негативных сценариев. Тесты только на happy path дают ложную уверенность. Проверяйте невалидные данные, ошибки сети, пустые ответы.
Отсутствие тестов в CI. Тесты, которые запускаются только локально, со временем перестают запускаться вообще. CI — обязательное условие.
Заключение
Внедрить тесты в проект без тестов — задача не на один день, а долгосрочная стратегия. Начните с настройки Vitest и CI-пайплайна. Напишите первые тесты для чистых функций и критичной бизнес-логики. Введите правило: новый код идёт только с тестами. Постепенно повышайте порог покрытия. Через несколько месяцев вы получите проект, в котором тесты — не обуза, а надёжная страховка от регрессий при каждом релизе.






Комментарии
0