Mutation testing. Как проверить проверяющего

## Образ

Здание увешано пожарными датчиками. Все горят зелёным. Означает ли это, что здание защищено от пожара?

Нет. Это означает только, что ни один датчик сейчас не кричит. Между «датчики молчат» и «пожара нет» лежит непроверенное допущение: что датчики вообще способны закричать. Единственный способ проверить это допущение — поднести дым. Намеренно, контролируемо, зная заранее, что датчик ОБЯЗАН сработать. Датчик, переживший поднесённый дым молча, — не датчик. Он декорация, которая до этой минуты числилась защитой.

Mutation testing — это дисциплина поднесения дыма.

## Слой 1. Механика

Классический mutation testing работает с кодом и его тестами:

1. Берётся программа, у которой все тесты зелёные.
2. В код вносится одно маленькое намеренное искажение — мутант: `>` заменяется на `>=`, `+` на `-`, константа сдвигается на единицу, ветка условия выбрасывается.
3. Тесты прогоняются заново.
4. Если хотя бы один тест упал — мутант убит (killed): набор тестов видит это искажение.
5. Если все тесты остались зелёными — мутант выжил (survived): в коде живёт заведомая ошибка, а набор тестов её не отличает от правильной программы.

Повторить для сотен мутантов. Доля убитых — mutation score. Каждый выживший мутант — конкретное, предъявляемое место слепоты: вот искажение, вот зелёный прогон, вот тест, которого не хватает.

## Слой 2. Что на самом деле испытывается

Ключевой сдвиг — в объекте испытания. Обычный тест спрашивает: «работает ли код?». Mutation testing спрашивает: «видит ли тест?»

Зелёный набор тестов — это декларация: «дефектов нет». Но у этой декларации есть скрытый квантор, который все читают неправильно. Она означает «нет дефектов, различимых этим набором», а читается как «нет дефектов вообще». Разница между этими двумя фразами — ровно то место, где живут все катастрофы зрелых систем: не там, где проверка упала, а там, куда она не смотрела, продолжая гореть зелёным.

Mutation testing — единственный систематический способ измерить этот зазор. Он не проверяет программу. Он проверяет наблюдателя программы. Покрытие строк (coverage) отвечает на вопрос «исполнялась ли строка под тестом»; mutation score отвечает на вопрос неизмеримо более сильный — «заметит ли тест, если эта строка соврёт». Строка может исполняться в тысяче прогонов и ни в одном не проверяться.

## Слой 3. Почему маленькие искажения — достаточно (теория)

Идея старая: Ричард Липтон, 1971; каноническая статья — DeMillo, Lipton, Sayward, «Hints on Test Data Selection» (1978). Она стоит на двух гипотезах, и обе стоит знать, потому что это допущения метода, а не законы природы.

Гипотеза компетентного программиста. Реальный код пишется людьми, которые почти правы: настоящие ошибки — это маленькие отклонения от корректной программы (перепутанный знак, сдвиг на единицу, забытое условие), а не случайный шум. Поэтому маленькие синтетические мутанты — репрезентативная модель настоящих ошибок.

Эффект сцепления (coupling effect). Тест, убивающий простые мутанты, с высокой вероятностью убивает и сложные ошибки, составленные из простых. Эмпирика десятилетий это в целом подтверждает. Следствие практическое: не нужно генерировать хитрые многоместные искажения — хватает простых операторов, применённых поодиночке.

Оба допущения — ставки, не теоремы. Они слабеют там, где ошибки НЕ малы синтаксически: ошибка замысла, ошибка онтологии, неверная модель предметной области. Мутант не поймает «мы вообще не то считаем». Это честная граница метода: он испытывает зоркость к искажениям исполнения, не к искажениям смысла.

## Слой 4. Экономика и промышленная реальность

Наивная цена чудовищна: N мутантов ; полный прогон тестов. На большой кодовой базе — комбинаторное пекло. Поэтому индустрия сходилась не к вопросу «делать ли», а к вопросу «как выбирать»:

- Инструменты: PIT/Pitest (Java, де-факто стандарт), Stryker (JS/TS/C#), mutmut/cosmic-ray (Python). Все зрелые, все бесплатные.
- Инкрементальность: мутировать только изменённый код (diff-based) — цена падает на порядки.
- Выборка: случайное подмножество мутантов даёт статистически честную оценку score без полного прогона.
- Google (Petrovi;, Ivankovi;, ~2018+): mutation testing на масштабе тысяч проектов — с предиктивным отбором мутантов и показом выживших прямо в код-ревью изменённых строк. Их главный урок: не гнаться за score, показывать разработчику конкретного выжившего мутанта в момент, когда контекст горячий. Мутант как реплика в диалоге ревью, а не как метрика в отчёте.
- Timeouts и эквиваленты: мутант может уронить программу в вечный цикл (лечится таймаутом) или оказаться эквивалентным — искажение, не меняющее поведения вообще (`x < 10` ; `x <= 9` при целом x). Эквивалентные мутанты неубиваемы по построению, их доля — неустранимый шум метода, и отличать их от настоящей слепоты — ручной труд. Это главная известная боль метода.

## Слой 5. Ловушки — прежде всего Гудхарт

Mutation score — метрика, а всякая метрика, ставшая целью, перестаёт мерить (закон Гудхарта).

- Требование «score ; 90%» порождает тесты, написанные ПОД мутантов: они убивают синтетические искажения и не выражают ни одного намерения о поведении. Формально зорче, по существу — тот же культ зелёного, этажом выше.
- Погоня за эквивалентными мутантами сжигает часы на неубиваемое.
- Правильное употребление — диагностическое, не целевое: выживший мутант — это вопрос («почему набор этого не видит?»), а не штраф. Иногда честный ответ — «и не должен видеть, ветка неважна»; тогда это решение записывается, а не замазывается тестом-заглушкой.

Тонкая ирония метода: он сам построен на недоверии к зелёному цвету — и сам же порождает новый зелёный цвет (score), которому нельзя доверять слепо. Проверка наблюдателя нуждается в собственном наблюдателе. Эта регрессия не порочна, она просто напоминает: последняя инстанция — всегда живое суждение о том, ЧТО важно проверять.

## Слой 6. Переизобретение снизу: подсадки

Есть наблюдение, которое говорит о методе больше, чем его пятидесятилетняя история: команды, всерьёз строящие контроль качества, приходят к нему сами, не зная имени. Мне довелось наблюдать это изнутри на одном производственном проекте — метод был переизобретён дважды, в двух независимых формах.

Первая форма — обязательные негативные фикстуры: ни один новый контролирующий механизм не считается установленным, пока не доказал остановку на известном плохом входе. Это ручные мутанты, встроенные в конвейер, — дым, подносимый к датчику при каждой установке.

Вторая форма получила у владельца проекта имя «испытание с подсадками»: прежде чем доверить упрощённой процедуре проверку большого пакета работ, в пакет подсаживают по известному дефекту каждого класса; поймала все — процедуре можно верить, пропустила хоть один — откат. Это mutation testing в чистом виде, с точностью до одного сдвига.

Сдвиг существенный: классический метод мутирует код, здесь мутируется продукт и рабочие пакеты — объекты, которые проверяют автоматические гейты. Испытывается не набор юнит-тестов, а целый парк проверяющих инструментов, чья зоркость иначе принимается на веру. Для системы, где главный исторический враг — проверка, чей локальный «pass» читается как глобальная чистота, это лекарство по рецепту: каждый такой инцидент был случаем «зелёный горит, дефект жив», и каждый был бы пойман подсадкой своего класса.

И второе отличие, которого нет в классике: память провалов как корпус мутантов. Стандартные инструменты генерируют мутантов синтаксическими операторами — вслепую, равномерно, с шумом эквивалентов. Но если проект дисциплинированно ведёт архив реальных провалов — с воспроизведением и указанием, какая проверка обязана была увидеть, — то подсадки, деривированные из этого архива, оказываются мутантами, взвешенными эмпирической историей: каждый уже случался, каждый уже стоил денег, и ни одного эквивалентного среди них нет по построению. Это дешевле и точнее случайной генерации — структурное преимущество, выросшее из дисциплины памяти.

Чего обычно не хватает переизобретённой форме — календарности. Подсадка случается как событие: при установке гейта, при разовом испытании. Но датчики стареют: проверка, написанная весной, живёт летом среди новых поверхностей и новых обходных путей. Штатный приём — регулярно брать по одному классу из архива провалов, инъецировать его дефект в копию продукта и требовать красного. Молчание гейта — находка ценнее среднего аудита: найдена не ошибка, а слепота. Стоимость — минуты, дефекты уже описаны; защита от Гудхарта — результат записывается как вопрос («почему выжил»), а не как процент в отчёте.

## Слой 7. Эпистемический остаток

Если снять с метода всю механику, остаётся принцип, который шире тестирования:

Всякий контролирующий слой рождает производную декларацию — «у нас есть контроль» — и эта декларация нуждается в проверке фактом ровно так же, как исходная. Тесты проверяют код; что проверяет тесты? Гейты проверяют продукт; что проверяет гейты? Ревью проверяет работу; что проверяет ревьюера? Я знаю этот вопрос не понаслышке: усталый ревьюер, начавший принимать отчёты, не открывая стоящих за ними артефактов, — это выживший мутант в ревьюере; искажение проходит, наблюдатель горит зелёным.

Ответ метода отрезвляюще прост: подноси дым. Не жди настоящего пожара, чтобы узнать, работает ли датчик; не жди настоящей порчи, чтобы узнать, смотрит ли гейт; подсунь известную ложь и посмотри, кто закричит. Зоркость нельзя задекларировать — её можно только регулярно предъявлять на заведомом искажении.

Декларация не равна факту — и это верно не только для отчётов о работе, но и для самих механизмов проверки отчётов. Mutation testing — индустриальное имя дисциплины, которая не позволяет слову «проверено» самому остаться непроверенным.


Рецензии