Skip to content
PПромтбук
RUEN
04Тестирование

Тесты для 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 (если есть хоть что-то) — где дыры

Приоритет покрытия:

  1. Часто меняется + критично для бизнеса — сначала
  2. Часто ломается — параллельно
  3. Стабильно, не трогаем — не тратим время
  4. Мёртвый код — кандидат на удаление, не на тесты

Фаза 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.

Алгоритм:

  1. Найди функцию, которую планируешь менять
  2. Подготовь реалистичный input
  3. Вызови, запиши вывод как есть (даже если он выглядит неправильно)
  4. Зафиксируй в assert: expect(result).toEqual(<копия вывода>)
  5. Если вывод выглядит "неправильно" — добавь 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 строк

  1. Characterization test на 5-10 входов
  2. Extract method для самой независимой части
  3. Unit test на extracted
  4. Повтор

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 недели: какие модули покрываем дальше
К подразделу «Тестирование»
Похожие промты