awesome-everything EN
↑ Обратно к восхождению

Инженерная практика

Мутационное тестирование — честная метрика качества тестов

Суть Покрытие строк говорит, что строка выполнилась, а не что тест заметит её поломку. Мутационное тестирование внедряет баги и проверяет, убивает ли их набор — можно достичь 100% покрытия с мутационным баллом 67%. Выжившие мутанты — это проверки, которых вам не хватает.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

Команда требует 100% покрытия строк и добивается его. Руководство успокоено. Затем кто-то запускает Stryker на том же модуле, и мутационный балл возвращается равным 67% — треть внедрённых багов выжила. Худший выживший: Stryker сменил граничную проверку с amount > limit на amount >= limit, прогнал весь набор, и каждый тест всё равно прошёл. Набор выполнял эту строку в каждом прогоне — покрытие 100% — но ни один тест ни разу не подал ровно то граничное значение, на котором > и >= различаются. Число покрытия измеряло, что строка выполнилась, но никогда — что тест заметит её поломку. Это не одно и то же, и зазор составил треть кода.

Покрытие измеряет выполнение, а не обнаружение

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

Мутационное тестирование измеряет то, что покрытие не может. Инструмент берёт ваш проходящий набор, вносит маленькое изменение в продакшен-код — мутанта — и перезапускает тесты. Меняет > на >=, + на -, && на ||, return x на return null, удаляет инструкцию. Если теперь тест падает, мутант убит — ваш набор обнаружил внедрённый баг. Если все тесты всё равно проходят, мутант выжил — ваш набор не заметил бы ровно эту поломку. Мутационный балл — это убитые ÷ (всего валидных мутантов), и, в отличие от покрытия, это прямое измерение силы обнаружения. Stryker (JS/TS, C#, Scala) и PIT (JVM) — стандартные инструменты; документация Stryker точно определяет состояния убит/выжил/таймаут/без-покрытия.

МетрикаНа какой вопрос отвечаетЧто упускает
Покрытие строкПрогнал ли тест эту строку?Проверяет ли хоть один тест поведение этой строки
Покрытие ветвейВыполнилась ли каждая ветвь?Тестировалось ли граничное значение, различающее ветви
Мутационный баллУпал бы тест, будь этот код неверным?Мало что — но он медленный и шумит эквивалентными мутантами

Выжившие мутанты — это ваши недостающие проверки, названные поимённо

Важен не балл, а список выживших мутантов. Каждый выживший — точное, действенное утверждение: «я изменил вот это, а твоим тестам всё равно». Граничный выживший — > стал >=, и ничего не упало — прямо говорит вам, что не хватает теста на граничном значении, самого частого класса реальных багов. Выживший &&|| говорит, что логическое условие недотестировано. Выжившее удаление инструкции говорит, что побочный эффект никогда не проверяется. Это и есть настоящий продукт мутационного тестирования: оно превращает «ваши тесты где-то слабы» в «добавь проверку здесь, на этой строке, для этого случая». Это честный ревьюер, который читает ваши проверки, а не отчёт о покрытии.

Окупается оно ровно там, где покрытие лжёт сильнее всего: критическая логика с границами, деньгами, правами, переходами состояний. Его запуск всплывает тесты, которые вы бы написали, додумайся вы до случая — это та же проблема слепых пятен, которую property-тестирование атакует со стороны входов. Эти два подхода дополняют друг друга: property-тесты генерируют входы, которые вы не перечислили; мутационное тестирование находит выходы, которые вы не проверили. Оба существуют потому, что покрытие строк измеряет не то.

Цена: время CPU и эквивалентные мутанты

Мутационное тестирование не бесплатно, и из-за цены команды его дозируют, а не гоняют на всём. Главная цена — время: инструмент перезапускает (подмножество) вашего набора по разу на мутанта, поэтому модуль с сотнями мутантов прогоняет ваши тесты сотни раз. Прогон мутаций по всей кодовой базе может занять часы, что неприемлемо на каждом коммите. Практический шаблон — дозировать: гонять на изменённых файлах в CI (Stryker и PIT оба поддерживают инкрементальные/diff-прогоны) или прогонять полный проход ночью на критических модулях, а не весь репозиторий на каждый push.

Вторая цена — проблема эквивалентного мутанта. Некоторые мутации дают код, поведенчески идентичный оригиналу — например, мутация i < n в i != n в цикле, который всегда инкрементирует на единицу, где оба завершаются одинаково. Ни один тест не может убить эквивалентного мутанта, потому что обнаруживать нечего, но он засчитывается против вашего балла и требует человеческого суждения, чтобы его отбросить. Обнаружение эквивалентных мутантов в общем случае неразрешимо, поэтому вы разбираете их вручную. Это и есть честная сеньорская оговорка: мутационный балл ниже 100% ожидаем, выживших надо читать, а не слепо гнаться за ними, и цель — убить значимых выживших на критическом коде, а не идеальное число на всём.

Почему это работает

Мутационное тестирование честная метрика потому, что оно тестирует тесты той же валютой, что и реальный баг: неверный оператор, перевёрнутое условие, удалённый побочный эффект. Инструмент покрытия удовлетворяется одним лишь выполнением, поэтому его можно обмануть — можно гнаться за 100%, вызывая код без проверок, и число растёт, тогда как набор сильнее не становится. Мутационный балл так не обмануть, потому что единственный способ убить мутанта — иметь проверку, которая реально отличает корректное поведение от внедрённого неверного. Вот почему выживший мутант ценнее процента покрытия: это не статистика о вашем коде, это конкретный баг, который ваш набор только что доказательно пропустил бы.

Выбери лучший вариант

У вашего биллинг-модуля 100% покрытия строк, но Stryker сообщает мутационный балл 67%, с выжившим граничным мутантом '>' → '>='. Какой ответ правильный?

Викторина

Как возможно иметь 100% покрытия строк и мутационный балл 67% на одном и том же коде?

Викторина

Почему сеньору не стоит гнаться за мутационным баллом 100% по всему репозиторию?

Расставь шаги по порядку

Упорядочьте, как мутационное тестирование вскрывает и закрывает пробел в тестах:

  1. 1 Ваш набор полностью зелёный с 100% покрытия строк
  2. 2 Инструмент внедряет мутанта: меняет '>' на '>=' в граничной проверке
  3. 3 Он перезапускает набор; каждый тест всё равно проходит — мутант выживает
  4. 4 Выживший называет пробел: ни один тест не подаёт граничное значение
  5. 5 Добавляете граничную проверку; перезапуск — и мутант теперь убит
Вспомните перед уходом
  1. 01
    Руководство доверяет гейту 100% покрытия. Объясните, почему мутационное тестирование, сообщающее 67% на том же коде, не противоречие.
  2. 02
    Как сеньору на деле внедрять мутационное тестирование с учётом его цены?
Итог

Покрытие строк отвечает на слабый вопрос — прогнал ли тест эту строку — и ничего не говорит о том, упал бы ли хоть один тест, будь строка неверной, поэтому набор может достичь 100% покрытия и всё равно пропустить треть внедрённых багов. Мутационное тестирование измеряет обнаружение напрямую: оно внедряет маленькие изменения (мутант — сменить ’>’ на ’>=’, ’+’ на ’-’, ’&&’ на ’||’, удалить инструкцию), перезапускает набор и сообщает мутанта убитым, если тест падает, или выжившим, если все проходят; мутационный балл — это убитые от всего числа валидных мутантов, а Stryker и PIT — стандартные инструменты. Реальный выход — список выживших, каждый из которых названная недостающая проверка — граничный выживший ’>’ → ’>=’ точно говорит, где добавить тест, для самого частого класса реальных багов. Окупается оно ровно там, где покрытие лжёт сильнее всего: границы, деньги, права, переходы состояний, и оно дополняет property-тестирование, которое атакует то же слепое пятно со стороны входов. Цена настоящая: время прогона, ведь набор перезапускается по разу на мутанта, а полные проходы занимают часы, поэтому дозируйте его до diff’ов в CI или ночью на критических модулях; и эквивалентные мутанты — поведенчески идентичные мутации, которых не убить ни одним тестом, что делает идеальный балл невозможным и требует ручного разбора. Сеньорская цель — убить значимых выживших на критическом коде, а не безупречное число на всём — мутационный балл честная метрика потому, что, в отличие от покрытия, его не удовлетворить выполнением без проверки.

Связанные уроки
Продолжить восхождение ↑TDD: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.