| Мутационное тестирование: не только юнит-тесты |
| 29.06.2026 00:00 |
|
Я уже несколько раз писал о мутационном тестировании в этом блоге, и даже частенько провожу воркшоп по мутационному тестированию. Одно из заблуждений, которое иногда всплывает — либо явно в вопросах, либо неявно, когда я слышу, как о мутационном тестировании говорит кто-то еще, — заключается в том, что мутационное тестирование работает только для юнит-тестов. Это не так. Мутационное тестирование можно использовать и для оценки качества других типов тестов. В этой статье я покажу пример на основе кодовой базы, которую я использую в своих воркшопах по мутационному тестированию. В конце статьи будет ссылка на репозиторий, чтобы можно было посмотреть и попробовать всё самостоятельно. Важно помнить: хотя мутационное тестирование подходит не только для юнит-тестов, лучше всего оно работает с тестами, которые выполняются быстро. Набор тестов будет запускаться для каждой мутации в продуктовом коде, которую сгенерирует инструмент мутационного тестирования, поэтому, если результатов не хочется ждать часы или даже дни, стоит использовать его с быстрыми тестами — с временем выполнения в миллисекундах, максимум одну-две секунды. Наше тестируемое приложение Чтобы показать, что мутационное тестирование работает и для других типов тестов, я написал API на Java с использованием Spring Boot, включающий слои controller, service и repository, с подключением к in-memory базе данных H2. API позволяет создавать, получать и удалять банковские счета, а также выполнять разные типы операций: пополнение счёта, снятие средств и начисление процентов. Вот реализация операции пополнения в контроллере и соответствующий код сервисного слоя, чтобы было понятно, как это выглядит: // From AccountController.java // From AccountService.java Разумеется, есть и соответствующий набор тестов, проверяющий реализацию этого API. Поскольку, повторюсь, моя цель — показать, что мутационное тестирование работает не только для юнит-тестов, используется набор приёмочных тестов, написанных с помощью REST Assured. Эти тесты поднимают локальный экземпляр API вместе с базой данных в памяти на машине, где выполняются тесты, поэтому время выполнения измеряется миллисекундами. Вот пример такого теста. Обратите внимание: «сырой» код REST Assured вынесен в клиент, чтобы тесты было проще читать, писать и поддерживать. @Test Метод createAccount() в клиентском классе, который выполняет HTTP POST-запрос для создания нового аккаунта, выглядит так: public int createAccount(AccountDto account) {Аналогично метод depositToAccount() выполняет ещё один HTTP-запрос с использованием REST Assured. После прочтения статьи можно посмотреть кодовую базу, чтобы лучше понять структуру. Запуск тестов и мутационного тестированияВ этом примере используется PIT как инструмент мутационного тестирования. Сначала запускаются тесты, чтобы убедиться, что все они проходят — нет смысла вычислять mutation score для падающих тестов. Используется команда mvn clean test. [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.847 s -- in com.ontestautomation.mutationbank.MutationBankApplicationTests Затем запускается мутационное тестирование с помощью PIT, используя команду mvn org.pitest:pitest-maven:mutationCoverage: ================================================================================ Как можно видеть, прогон мутационного тестирования занял меньше минуты, что вполне приемлемо. Да, это небольшая кодовая база, но даже в этом случае набор приёмочных тестов (9 тестов) был выполнен 55 раз менее чем за минуту. Это подтверждает, что мутационное тестирование полезно не только для юнит-тестов. Анализ отчётовЕсли посмотреть на результаты мутационного тестирования и сосредоточиться на логике метода processDeposit() и созданных PIT мутациях, можно увидеть следующее:
Очевидно, часть бизнес-логики недостаточно покрыта тестами. В частности, нет проверки, что попытка внести неположительное значение на счёт приводит к выбросу BadRequestException (что, в свою очередь, даёт HTTP 400). Добавление нового теста для улучшения mutation scoreЧтобы «убить» мутации, которые до сих пор выжили, добавим тест, проверяющий попытку внести неположительное значение. Также покрывается граничное значение — добавляется тест с пополнением на 0: @Test После запуска тестов (новый тест тоже проходит) и повторного запуска мутационного тестирования видно, что время всё ещё меньше минуты, хотя тестов стало больше — 10 вместо 9: ================================================================================ Если посмотреть отчёт, видно, что mutation score для этой части API тоже улучшился:
Это определённо шаг в правильном направлении: теперь у нас есть тест, который не только улучшает покрытие кода, но и гарантированно упадёт, если разработчик случайно изменит логику пополнения и разрешит вносить ноль или отрицательные значения. Отлично! Попробуйте самиЕсли хочется попробовать мутационное тестирование на той же кодовой базе, она доступна на GitHub. В ней есть всё: тестируемый API, приёмочные тесты и инструмент мутационного |