Тесты для legacy без тестов
Где врезаться в код без тестов, как написать characterization tests и наращивать покрытие безопасно.
Начни покрывать тестами legacy {{module}}. Стек: {{stack}}.
Правило #1: legacy = код без тестов. Любое изменение опасно — даже "просто переименовать". Тесты идут ПЕРЕД любым рефакторингом.
Правило #2: не пытайся покрыть всё. Покрывай то, что меняешь и то, что ломается.
Фаза 1. Карта рисков
Сделай inventory без чтения кода целиком:
- Список файлов с размером (строки) и датой последнего изменения
- Топ-10 файлов по
git log --since="6 months ago" -- <file> | wc -l— где правки чаще всего - Bug tracker за полгода: какие модули чаще ломаются
- Coverage report (если есть хоть что-то) — где дыры
Приоритет покрытия:
- Часто меняется + критично для бизнеса — сначала
- Часто ломается — параллельно
- Стабильно, не трогаем — не тратим время
- Мёртвый код — кандидат на удаление, не на тесты
Фаза 2. Точки врезания (seams)
Legacy сопротивляется тестам потому что зависимости зашиты. Ищи seams — места, где можно подменить поведение без правок:
- Object seam — метод/функция, которую можно подменить subclass'ом или DI
- Preprocessing seam — макрос/декоратор/прокси
- Link seam — подмена модуля на уровне импорта (
jest.mock, monkey-patch)
Если seam'ов нет — нужен sprout method / sprout class: новый код пишешь в отдельной функции, которую можно тестировать, старый трогаешь минимально (один call site).
Фаза 3. Characterization tests
Это тесты, которые описывают что код делает сейчас, а не что должен делать. Их задача — safety net.
Алгоритм:
- Найди функцию, которую планируешь менять
- Подготовь реалистичный input
- Вызови, запиши вывод как есть (даже если он выглядит неправильно)
- Зафиксируй в assert:
expect(result).toEqual(<копия вывода>) - Если вывод выглядит "неправильно" — добавь TODO, не правь
Цель: если кто-то случайно изменит поведение — тест упадёт. Если поведение бажное — это бизнес знает; чинят отдельной задачей.
// characterization, не unit
it('characterization: calculatePrice returns existing behaviour', () => {
const result = calculatePrice({ items: [...], region: 'EU', coupon: 'X' });
// TODO: проверить что 99.95 действительно правильно — пока фиксируем как есть
expect(result).toEqual({ total: 99.95, currency: 'EUR', taxApplied: false });
});
Фаза 4. Golden master / snapshot
Когда функция сложная и нет смысла assert'ить каждое поле:
- Прогнал на N реальных входах (sample из prod logs, анонимизированный)
- Записал вывод в файл (
golden/case-01.json) - Тест читает input, прогоняет, сравнивает с golden
Используй для: рендеринга, transformations, генераторов отчётов.
Фаза 5. Контактные тесты (smoke + contract)
Перед углублением — широкий smoke layer:
- "Главные эндпойнты возвращают 2xx на типовом input"
- "CLI с базовыми флагами не падает"
- "Главные джобы запускаются и не кидают"
Это не unit, это якорь — если эти упадут, мы знаем что упало рано.
Фаза 6. Incremental coverage
Правило ratchet: coverage может только расти.
- Замерь baseline (например 12%)
- CI блокирует merge если упало
- Каждый PR трогает legacy → добавляет тесты на трогаемые куски
Не цель — 80% за неделю. Цель — за 6 месяцев пройти с 12% до 60% по тем модулям, что действительно меняются.
Шаблоны для частых пейнов
A. Бог-функция на 500 строк
- Characterization test на 5-10 входов
- Extract method для самой независимой части
- Unit test на extracted
- Повтор
B. Зависит от глобалей / синглтонов
- Sprout: новая функция принимает зависимости параметрами
- Старый код вызывает sprout, передавая глобаль
- Тесты пишутся на sprout
C. Сетевые / DB зависимости
- Test container (Postgres в Docker) для integration
- Wiremock / MSW для HTTP
D. Время / случайность
- Внедри
Clock/RandomSourceинтерфейс - В тестах подменяй на детерминированный
Анти-паттерны
- Покрывать "по файлам сверху вниз" — покроешь то, что не меняется
- Писать сначала unit'ы внутрь private методов — хрупкие, ломаются на рефакторе
- Snapshot на UI без ревью — passive testing, ловит ноль
- Гнаться за coverage % — 90% coverage с assert'ами уровня
expect(x).toBeTruthy()ничего не доказывает - Чинить найденный баг "заодно" с написанием теста — теряешь safety net
В конце
- Карта приоритетов (топ-10 модулей и почему)
- Список seams в {{module}}
- 3-5 characterization tests готовых к мержу
- Baseline coverage и ratchet-правило в CI
- План на 4 недели: какие модули покрываем дальше
Стратегия внутренней перелинковки
Граф ссылок: какие страницы — хабы, какие — спицы, как передавать link equity.
North-Star метрика и input-метрики
Одна главная метрика которая отражает успех продукта + 3-5 драйверов под неё.
Позиционирование продукта
Одна формула: для [кого] · которые [боль] · продукт — это [категория] · который [ценность] · в отличие от [альтернатив].