Раздел 29 · Тестирование

Mutation testing: честный прибор и приёмка ИИ-тестов

middle-senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Mutation testing: честный прибор и приёмка ИИ-тестов

Сцена · подсадить опечатку

Возьми рабочий код и специально внеси в него мелкий баг: поменяй <= на <, верни пустой список вместо настоящего. Теперь запусти тесты. Если хоть один покраснел, набор поймал диверсию, тест живой. Если все остались зелёными, тест декоративный: он исполняет строки, но ничего не проверяет.

Делать это руками ты научился ещё в уроке про эвристики качества, эвристикой “сломай код”. Mutation testing это та же идея, доведённая до автомата: инструмент сам сажает в код сотни таких опечаток и считает, сколько из них поймано. Это и есть честный прибор, обещанный ещё в уроке про coverage.

Карта урока · что заберёшь

  • поймёшь, что меряет mutation score и чем он честнее coverage;
  • разберёшь, как Stryker сажает мутантов и читается его отчёт;
  • научишься превращать выжившего мутанта в задачу;
  • замкнёшь центральную петлю серии: mutation как приёмка ИИ-тестов.

Раздел 1 · что меряет mutation testing

Идея словами

Мутант это копия кода с одним маленьким изменением. Инструмент создаёт их пачками: меняет операторы, константы, условия. Потом на каждом мутанте гоняет тесты. Mutation score это доля убитых.

Разница с coverage принципиальная. Coverage отвечает на вопрос “исполнилась ли строка”. Mutation score отвечает на вопрос “заметит ли набор, если эту строку испортить”. Первое легко обмануть тестом без единого осмысленного expect. Второе нет: чтобы убить мутанта, ассерт обязан реально что-то проверять.

Картинка · мутанты против набора

Слева coverage честно показывает 100 процентов: каждая строка validatePayment исполнена. А мутанты выживают: поменяли <= 0 на < 0, и ни один тест не заметил, потому что случай с нулём никто не проверял. Нажми “Усилить тесты”: добавили проверки на ноль и на пустой список причин, выжившие мутанты гибнут, mutation score доезжает до 100. Coverage при этом не двигался: он этой разницы вообще не видит.

Что взять с собой

  • coverage меряет исполнение строк, mutation score силу ассертов;
  • чтобы убить мутанта, тест обязан реально проверять результат;
  • 100 процентов coverage и низкий mutation score это норма для пустого набора.

Раздел 2 · Stryker за минуту

Идея словами

В нашем стеке mutation testing делает Stryker: он берёт исходники, генерирует мутантов, запускает на них Vitest и собирает HTML-отчёт с разбивкой убитых и выживших.

Шаг 1 · конфиг

// packages/ledger/stryker.conf.json
{
  "packageManager": "pnpm",
  "testRunner": "vitest",
  "reporters": ["html", "clear-text", "progress"],
  "coverageAnalysis": "perTest",
  "mutate": ["src/**/*.ts", "!src/index.ts"],
  "thresholds": { "high": 90, "low": 80, "break": 70 }
}

Конфиг лежит в самом пакете домена, поэтому пути в mutate относительны его src. mutate очерчивает, что мутировать: чистый домен, без реэкспортного index.ts. thresholds.break это порог провала: mutation score ниже 70 валит запуск, как coverage gate из прошлого урока про CI, только честнее.

Шаг 2 · запуск и отчёт

pnpm mutation   # stryker run

В HTML-отчёте каждый выживший мутант подсвечен прямо в коде: видно строку и то, во что её превратили. Это не абстрактный процент, а конкретный список “вот сюда можно внести баг, и никто не заметит”.

Что взять с собой

  • Stryker сажает мутантов и гоняет на них Vitest;
  • mutate очерчивает зону, thresholds.break это порог провала;
  • HTML-отчёт показывает каждого выжившего прямо в коде.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.