Лови мутанта! Мутационные тесты: зачем и как |
06.06.2023 00:00 |
Автор: Сергей Никитченко
Я очень люблю тесты и считаю, что любой код должен быть покрыт ими, желательно качественными :) Поэтому хочу поделиться с вами опытом внедрения мутационных тестов в проект, рассказать зачем оно нужно и какую ценность несет. Рассмотрим пример внедрения Infection в приложение на Laravel. Но сначала немного теории. Что такое мутационные тесты? По факту это тесты для тестов. Все прекрасно понимают, что наличие тестов в проекте само по себе еще ничего не гарантирует: ни покрытие, ни отсутствие ошибок в коде и самих тестах. Важно качество этих тестов, что они действительно проверяют то, что должны и все дополнительные сценарии. Работает это так: мутационный фреймворк изменяет исходный код проекта согласно определенному набору правил, например, меняет “ Зачем их внедрять?Разработчики склонны писать “позитивные” тесты, т.е. чаще проверяют, что код работает в нормальном сценарии, а не пытаются нарушить его работу некорректными данными. Получается, качество тестов сильно зависит от конкретного разработчика и качества ревью в команде. Получается, что довольно сложно следить за качеством тестов. Частично эту проблему может решить оценка покрытия кода тестами. Но только на первых порах - то, что строка кода вызывается в тесте далеко не значит, что тест проверяет ее :) Да и ошибки бывают не только в основном коде приложения, но и в самих тестах. Например, мне мутационные тесты помогли отловить несколько ошибок, где путались сущности в тесте. Но у них был одинаковый ID, поэтому тесты проходили, но логику проверяли неправильно. А уж сколько оказалось тестов, которые по факту не проверяли, что сущность действительно появилась в БД.. Что для этого нужно?В первую очередь у вас должны быть тесты. Их необязательно должно быть много, и ваш проект не должен быть полностью ими покрыт. Понятно, что если тесты упорно не писались с самого начала на каждый модуль, в любом более-менее большом проекте будет куча непокрытого кода.
Вывод будет содержать список файлов и строки, непокрытые тестами. Это тоже достаточно полезно - это значит, что данные строке не вызываются при запуске тестов. Как писать тесты?Нужно принять для себя, что тесты - это тоже часть проекта. На них должны распространяться те же стандарты качества кода. Там не должно быть костылей, копипасты и прочих нехороших вещей, которые в коде приложения хороший программист себе не позволит. Но у многих к тестам снисходительное отношение - лишь бы запускались. Лично я в первую очередь рекомендую писать функциональные тесты (тестирование черным ящиком ручек API) по нескольким причинам. Во-первых: такие тесты менее хрупкие, и при изменении внутренностей вам не нужно переписывать тесты. Тесты переделывать придется очень редко, только когда меняется API или бизнес-требования. Во-вторых: тесты проверяют всю функциональность целиком, включая роутинг, middleware, обработку параметров запроса и ответ приложения. Нас не сильно успокоит, что методы какого-то класса работают, если в целом метод API ломается из-за кривого роутинга, например. Если в коде есть взаимодействие со сторонними сервисами (платежная система, например), слой взаимодействия должен реализовывать программный интерфейс и вызываться в коде через DI контейнер по нему. А для теста реализовать этот интерфейс с заранее заготовленными данными для конкретных сценариев и подменить боевую реализацию сервиса в контейнере этой заглушкой. Ну и конечно тесты должны запускаться в CI после каждого коммита, иначе смысла в них нет. От теории к практикеПосмотрим, как это все выглядит на Laravel приложении. Сначала нужно установить мутационный фреймворк, там все очень просто: https://infection.github.io/guide/installation.html При первом запуске infection, он спросит, какие папки будем анализировать и создаст конфиг с ними.
Timeout ограничивает время обработки одного мутанта, если он будет слишком низкий часть мутантов будут пропускаться, если расчетное время их обработки больше, чем таймаут. ОптимизацияЯ думаю, вы уже поняли, что проход тестов по тысячам мутантов дело не быстрое :) Но нам на помощь приходит распараллеливание. В первую очередь нам нужно научиться просто запускать тесты в многопоточном режиме, просто добавив флаг В моем случае на небольшом приложении тесты стали проходить в пять раз быстрее. На больших проектах это может существенно улучшить жизнь. Ну и по аналогии в несколько потоков можно запускать мутационные тесты параметром Что дальше?Когда у вас сгенерировался первый отчет, не надо пугаться, что 90% мутантов выживает. Тут работает правило 20/80, вы быстро сможете прикрыть незакрытые куски и найти ошибки в тестах. Да и часть мутантов можно игнорировать, например из-за особенностей Laravel проходит мутант, изменяющий видимость экшена на protected, это конечно править не нужно. ВыводыМутационные тесты очень полезный инструмент. На мой взгляд - это единственный способ контролировать качество тестов. И чем ответственнее проект, тем важнее наличие качественных тестов. В моем случае это парольный менеджер BearPass[ссылка удалена модератором], тут уж точно без хороших тестов никуда. Сергей Никитченко, SVK.Digital |