Перейти к содержанию
Innovialabs

Как покрыть тестами легаси-код: характеризационные тесты и швы

Продвинутое3 мин

Коротко

Сначала зафиксируйте текущее поведение характеризационными или snapshot-тестами, затем через швы отрежьте базу, сеть и время, и только после этого меняйте код маленькими проверенными шагами.

Главная идея

В легаси-коде нельзя сразу писать тесты «как должно быть»: никто точно не знает, как должно быть, а код уже работает у пользователей. Поэтому порядок другой:

  1. Зафиксировать текущее поведение — даже странное.
  2. Найти швы, чтобы изолировать код от базы, сети, времени и файлов.
  3. Менять код маленькими шагами, запуская тесты после каждого.

Цель первых тестов — не доказать правильность, а получить сигнализацию: если поведение изменилось, вы узнаете об этом сразу.

Характеризационные тесты

Характеризационный тест описывает, что код делает сейчас. Техника простая:

  1. Вызовите функцию с реальными входными данными.
  2. Напишите проверку с заведомо неверным ожидаемым значением.
  3. Запустите тест, посмотрите фактический результат и вставьте его в ожидание.

Если результат кажется ошибкой, не исправляйте его сразу. Оставьте тест как есть, добавьте комментарий и заведите задачу: исправление бага — отдельное, осознанное изменение.

Выбирайте входы, которые проходят по разным веткам. Отчёт о покрытии (coverage) подскажет, какие ветки ещё не задеты.

Snapshot и approval testing

Когда результат большой — HTML, JSON, отчёт, PDF в текстовом виде, — писать ожидания вручную долго. Здесь помогают:

  • Snapshot-тесты — Jest toMatchSnapshot, в Python, например, syrupy. Первый запуск сохраняет результат, следующие сравнивают с ним.
  • Approval testing — библиотеки семейства ApprovalTests. Результат пишется в файл received, вы просматриваете diff и «одобряете» его, превращая в approved.
  • Golden master — прогон множества входов (например, выгрузки реальных данных без персональной информации) с сохранением всех выходов.

Следите, чтобы в снимок не попадали изменчивые данные: текущее время, случайные ID, порядок ключей. Их нужно подменять или нормализовать — иначе тесты будут «мигать».

Швы и разрыв зависимостей

Шов (seam) — место, где можно изменить поведение кода, не редактируя его в этом месте. Основные виды:

ШовКак выглядит
Объектныйпередать в конструктор или функцию другой объект
Модульныйподменить импорт: jest.mock, monkeypatch в pytest
Конфигурационныйдругой URL базы или флаг окружения для тестов

Типичный легаси-код сам тянет зависимости:

def send_reminders():
    for inv in db.query_unpaid():
        if inv.due_date < datetime.now() - timedelta(days=7):
            mailer.send(inv.email, render(inv))

Безопасный приём — извлечь параметры и оставить старую функцию обёрткой, чтобы не трогать вызывающий код:

def send_reminders_core(invoices, now, send):
    for inv in invoices:
        if inv.due_date < now - timedelta(days=7):
            send(inv.email, render(inv))

def send_reminders():
    send_reminders_core(db.query_unpaid(), datetime.now(), mailer.send)

Теперь send_reminders_core тестируется со списком в памяти, фиксированной датой и фейковой функцией отправки. Такие механические шаги лучше делать средствами IDE — автоматический рефакторинг меньше рискует ошибиться.

Ещё два полезных приёма:

  • Sprout method — новую логику пишите в отдельной функции с тестами и вызывайте из старого кода.
  • Wrap method — оборачиваете старую функцию новой, которая добавляет поведение до или после.

Рефакторинг малыми шагами

  • Одно изменение — один запуск тестов — один коммит.
  • Не смешивайте рефакторинг и изменение поведения в одном коммите.
  • Если тест упал и причина непонятна, откатитесь, а не чините вперёд.
  • По мере того как код становится понятнее, заменяйте снимки точными unit-тестами с осмысленными проверками.
  • Начинайте с участков, которые чаще всего меняются или ломаются, а не со всего проекта сразу.

Типичные ошибки

  • Большой рефакторинг «заодно» до появления тестов.
  • Снимки, которые обновляют не глядя, — тогда они ничего не защищают.
  • Моки на каждый вызов, из-за которых тест повторяет реализацию и ломается от любой правки.

Частые вопросы

Начните с более высокого уровня: end-to-end или API-тест, который фиксирует поведение снаружи. Он медленнее, но позволяет безопасно выделить первые швы и затем спуститься к unit-тестам.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.