Юнит тесты помогают нам удостовериться, что код работает так, как мы этого хотим. Одной из метрик тестов является процент покрытия строк кода (Line Code Coverage). Но насколько корректен данный показатель? Имеет ли он практический смысл и можем ли мы ему доверять? Ведь если мы удалим все assert строки из тестов, или просто заменим их на assertSame(1, 1), то по-прежнему будем иметь 100% Code Coverage, при этом тесты ровным счетом не будут тестировать ничего. Насколько вы уверены в своих тестах? Покрывают ли они все ветки выполнения ваших функций? Тестируют ли они вообще хоть что-нибудь? Ответ на этот вопрос даёт мутационное тестирование.
Мутационное тестирование (MT, Mutation Testing, Mutation Analysis, Program mutation, Error-based testing, Fault-based testing strategy) - это вид тестирования ПО методом белого ящика, основанный на всевозможных изменениях (мутациях) частей исходного кода и проверке реакции на эти изменения набора автоматических юнит тестов. Изменения в мутантной программе сохраняются крайне небольшими, поэтому это не влияет на общее исполнение программы. Если тесты после изменения кода не падают (failed), значит либо этот код недостаточно покрыт тестами, либо написанные тесты бесполезны. Критерий, определяющий эффективность набора автоматических тестов, называется Mutation Score Indicator (MSI).
Введем некоторые понятия из теории мутационного тестирования:
Для применения этой технологии у нас, очевидно, должен быть исходный код (source code), некоторый набор тестов (для простоты будем говорить о модульных - unit tests). После этого можно начинать изменять отдельные части исходного кода и смотреть, как реагируют на это тесты. Одно изменение исходного кода будем называть Мутацией (Mutation). Например, изменение бинарного оператора "+" на бинарный "-" является мутацией кода. Результатом мутации является Мутант (Mutant) - то есть это новый мутированный код. Каждая мутация любого оператора в вашем коде (а их сотни) приводит к новому мутанту, для которого должны быть запущены тесты. Кроме изменения "+" на "-", существует множество других мутационных операторов (Mutation Operator, Mutator, faults or mutation rules), каждый из которых имеет свою цель и применение:
- Мутация значений (Value mutation): изменение значения параметра или константы;
- Мутация операторов (Statement mutation): реализуется путем редактирования, удаления или перестановки оператора;
- Мутация решения (Decision Mutation): изменение логических, арифметических и реляционных операторов.
В зависимости от результата теста мутанты делятся на:
- Выжившие мутанты (Survived Mutants): мутанты, которые все еще живы, то есть не обнаруживаются при выполнении теста. Их также называют live mutants;
- Убитые мутанты (Killed Mutants): мутанты, обнаруженные тестами;
- Эквивалентные мутанты (Equivalent Mutants): мутанты, которые изменив части кода не привели к какому-либо фактическому изменению в выводе программы, т.е. они эквивалентны исходному коду;
- Нет покрытия (No coverage): в этом случае мутант выжил, потому что для этого мутанта не проводились тесты. Этот мутант находится в части кода, не затронутой ни одним из ваших тестов. Это означает, что наш тестовый пример не смог его охватить;
- Тайм-аут (Timeout): выполнение тестов с этим активным мутантом привело к тайм-ауту. Например, мутант привел к бесконечному циклу в вашем коде. Не обращайте внимание на этого мутанта. Он считается «обнаруженным». Логика здесь в том, что если этот мутант будет внедрен в ваш код, ваша CI-сборка обнаружит его, потому что тесты никогда не завершатся;
- Ошибка выполнения (Runtime error): выполнение тестов привело к ошибке (а не к провалу теста). Это может произойти, когда средство запуска тестов не работает. Например, когда средство выполнения теста выдает ошибку OutOfMemoryError или для динамических языков, когда мутант привел к неразборчивому коду. Не тратьте слишком много внимания на этого мутанта. Он не отображается в вашей оценке мутации;
- Ошибка компиляции (Compile error): это состояние возникает, когда это компилируемый язык. Мутант привел к ошибке компиляции. Он не отражается в оценке мутации, поэтому вам не нужно уделять слишком много внимания изучению этого мутанта;
- Игнорируется (Ignored): мы можем видеть это состояние, когда пользователь устанавливает конфигурации для его игнорирования. Он будет отображаться в отчетах, но не повлияет на оценку мутации;
- Тривиальные мутанты (Trivial Mutants): фактически ничего не делают. Любой тестовый пример может убить этих мутантов. Если в конце тестирования остались тестовые примеры, значит, это недопустимый мутант (invalid mutant).
Метрики:
- Обнаруженные (Detected): это количество мутантов, обнаруженных нашим тестом, то есть убитых мутантов. Detected = Killed mutants +Timeout;
- Необнаруженные (Undetected): это количество мутантов, которые не были обнаружены нашим тестом, то есть выживших мутантов. Undetected = Survived mutants + No Coverage;
- Покрытые (Covered): это количество мутантов покрытых тестами. Covered = Detected mutants + Survived mutants;
- Действительные (Valid): это количество действительных мутантов, не вызвавших ошибки компиляции или рантайма. Valid = Detected mutants + Undetected mutants;
- Недействительные (Invalid): это количество всех недействительных мутантов, т.е. они не могли быть протестированы, так как вызывали ошибку компиляции или выполнения. Invalid = Runtime errors + Compile errors;
- Всего мутантов (Total mutants): содержит всех мутантов. Total = Valid + Invalid + Ignored;
- Оценка мутации на основе покрытого кода (Mutation score based on covered code): оценивает общий процент убитых мутантов на основе покрытия кода. Mutation score based on covered code = Detected / Covered * 100;
- Неправильный синтаксис (Incorrect syntax): их называют мертворожденными мутантами, это представлено как синтаксическая ошибка. Обычно эти ошибки должен обнаруживать компилятор;
- Оценка мутации (Mutation score): это оценка, основанная на количестве мутантов. В идеале равна 1 (100%). Mutation score = Detected / Valid * 100 (%).
Источники:
Доп. материал: