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 — индустриальное имя дисциплины, которая не позволяет слову «проверено» самому остаться непроверенным.
Свидетельство о публикации №226071500893