Как оценить качество тестов? Многие полагаются на самый популярный, всем известный показатель — покрытие кода.
Но это количественный, а не качественный показатель.
Он показывает, какая часть вашего кода покрыта тестами, но не показывает, насколько хорошо эти тесты написаны.
Один из способов выяснить это — провести мутационное тестирование.
Внося небольшие изменения в исходный код и затем снова запуская тесты, этот инструмент позволяет выявить бесполезные тесты и некачественное покрытие.
На Встреча Badoo PHP в марте Я рассказал о том, как организовать мутационное тестирование PHP-кода и с какими проблемами вы можете столкнуться.
Видео доступно на связь , а за текстовую версию добро пожаловать под кат.
Что такое мутационное тестирование
Чтобы объяснить, что я имею в виду, я покажу вам пару примеров.Они просты, иногда преувеличены и могут показаться очевидными (хотя реальные примеры обычно довольно сложны и не видны невооруженным глазом).
Рассмотрим ситуацию: у нас есть элементарная функция, утверждающая, что определенный человек является взрослым, и есть тест, который это проверяет. В тесте есть dataProvider, то есть он тестирует два случая: возраст 17 и возраст 19. Я думаю, для многих из вас очевидно, что isAdult имеет 100% охват. Единственная линия.
Выполняется тестом.
Все отлично.
Но при более внимательном рассмотрении выясняется, что наш провайдер плохо написан и не проверяет граничные условия: возраст 18 лет не проверяется как граничное условие.
Вы можете заменить знак > на > =, и тест не уловит такое изменение.
Другой пример, немного более сложный.
Существует функция, которая создает простой объект, содержащий сеттеры и геттеры.
У нас есть три поля, которые мы устанавливаем, и у нас есть тест, который проверяет, что функция buildPromoBlock действительно создает ожидаемый нами объект.
Если присмотреться, у нас также есть setSomething, который устанавливает для некоторого свойства значение true. Но у нас в тесте такого утверждения нет. То есть мы можем убрать эту строку из buildPromoBlock — и наш тест не отловит это изменение.
При этом в функции buildPromoBlock мы имеем 100% покрытие, потому что во время теста все три строки выполнились.
Эти два примера подводят нас к тому, что такое мутационное тестирование.
Прежде чем разбирать алгоритм, дам краткое определение.
Мутационное тестирование — это механизм, который позволяет нам, внеся незначительные изменения в код, имитировать действия злого Буратино или Младшего Васи, которые пришли и начали целенаправленно ломать его, заменяя знаки > на <, = with !=, and so on. For each such change, made by us for good purposes, we run tests that should cover the changed string. Если тесты нам ничего не показали, если они не упали, то, вероятно, они недостаточно эффективны.
Они не тестируют крайние случаи, не содержат утверждений: возможно, их нужно улучшить.
Если тесты проваливаются, значит, они великолепны.
Они действительно защищают от таких изменений.
Поэтому наш код сложнее взломать.
Теперь давайте посмотрим на алгоритм.
Это довольно просто.
Первое, что мы делаем для проведения мутационного тестирования, — это берем исходный код. Далее мы получаем покрытие кода, чтобы знать, какие тесты запускать для какой строки.
После этого просматриваем исходный код и генерируем так называемых мутантов.
Мутант — это одно изменение кода.
То есть мы берем некую функцию, где в сравнении был знак > , в if меняем этот знак на > = — и получаем мутанта.
После этого запускаем тесты.
Вот пример мутации (мы заменили > на > =):
При этом мутации производятся не случайным образом, а по определенным правилам.
Реакция на мутационное тестирование идемпотентна.
Независимо от того, сколько раз мы запускаем мутационное тестирование одного и того же кода, оно дает одни и те же результаты.
Последнее, что мы делаем, — это запускаем тесты, охватывающие мутировавшую линию.
Давайте вынесем это из поля зрения.
Есть неоптимальные инструменты, которые проводят все тесты.
Но хороший инструмент будет запускать только те, которые необходимы.
После этого оцениваем результат. Анализы упали, значит все в порядке.
Если они не падают, значит, они не очень эффективны.
Метрики
Какие показатели дает нам мутационное тестирование? Это добавляет к покрытию кода еще три, о которых мы сейчас поговорим.Но сначала давайте разберемся с терминологией.
Есть понятие убитые мутанты: это те мутанты, которых наши тесты «пригвоздили» (то есть поймали).
Существует понятие сбежавший мутант (выжившие мутанты).
Это мутанты, которым удалось избежать наказания (то есть тесты их не поймали).
И есть понятия покрытый мутант - мутант, покрытый тестами, и его обратный непокрытый мутант, который вообще не покрыт никаким тестом (т.е.
у нас есть код, у него есть бизнес-логика, мы можем его изменить, но ни одного тест заключается в том, что они не проверяют изменения).
Основным показателем, который дает нам мутационное тестирование, является MSI (показатель мутаций), отношение количества убитых мутантов к их общему количеству.
Второй показатель — покрытие кода мутации.
Он именно качественный, а не количественный, потому что показывает, сколько бизнес-логики, которую можно ломать и делать на регулярной основе, улавливается нашими тестами.
И последний показатель покрыт MSI, то есть более мягким MSI. В этом случае мы рассчитываем MSI только для тех мутантов, которые были охвачены тестами.
Проблемы с мутационным тестированием
Почему об этом инструменте слышали менее половины программистов? Почему его не используют везде?Низкая скорость
Первая проблема (одна из основных) — скорость мутационного тестирования.В коде, если у нас есть десятки операторов мутации, даже для самого простого класса мы можем сгенерировать сотни мутаций.
Тесты необходимо будет проводить для каждой мутации.
Если у нас есть, скажем, 5000 модульных тестов, выполнение которых занимает десять минут, тестирование мутаций может занять несколько часов.
Что можно сделать, чтобы это нивелировать? Запускайте тесты параллельно, в нескольких потоках.
Распределите потоки по нескольким машинам.
Оно работает. Второй метод — инкрементные прогоны.
Нет необходимости каждый раз рассчитывать показатели мутации для всей ветки — можно взять ветку diff. Если вы используете ветки функций, вам будет легко это сделать: запускайте тесты только на тех файлах, которые изменились, и смотрите, что сейчас происходит в вашем мастере, сравнивайте, анализируйте.
Следующее, что вы можете сделать, это настроить мутации.
Поскольку операторы мутаций можно менять, вы можете устанавливать определенные правила, по которым они работают, вы можете прекратить выполнение некоторых мутаций, если они явно приводят к проблемам.
Важный момент: мутационное тестирование подходит только для модульных тестов.
Несмотря на то, что его можно использовать для интеграционных тестов, это, очевидно, плохая идея, поскольку интеграционные (например, сквозные) тесты выполняются намного медленнее и включают гораздо больше кода.
Вы просто никогда не получите результатов.
В принципе, этот механизм был придуман и разработан исключительно для модульного тестирования.
Бесконечные мутанты
Вторая проблема, которая может возникнуть при тестировании мутаций, — это так называемые бесконечные мутанты.
Например, есть простой код, простой цикл for:
Если вы замените i++ на i--, цикл станет бесконечным.
Ваш код застрянет на долгое время.
И мутационное тестирование довольно часто порождает такие мутации.
Первое, что вы можете сделать, это настроить мутацию.
Очевидно, что менять i++ на i-- в цикле for — очень плохая идея: в 99% случаев мы окажемся в бесконечном цикле.
Именно поэтому мы запретили это делать в нашем инструменте.
Второе и самое главное, что защитит вас от подобных проблем – это тайм-аут на пробежку.
Например, PHPUnit имеет возможность завершить тест по таймауту независимо от того, где он завис.
PHPUnit назначает обратные вызовы через PCNTL и сам рассчитывает время.
Если тест терпит неудачу в течение определенного периода времени, он просто убивает его, и такой случай считается убитым мутантом, потому что код, сгенерировавший мутации, фактически проверяется тестом, который фактически выявляет проблему, указывая на то, что код стал нерабочим.
работающий.
Идентичные мутанты
Эта проблема существует в теории мутационного тестирования.На практике встречается не очень часто, но знать об этом нужно.
Давайте посмотрим на классический пример, иллюстрирующий это.
У нас есть переменная A, умноженная на -1, и A, разделенная на -1. В целом эти операции приводят к одному и тому же результату.
Изменяем знак А.
Соответственно, имеем мутацию, позволяющую менять два знака между собой.
Логика программы такая мутация не нарушает. Тесты не должны его ни ловить, ни падать.
Из-за таких идентичных мутантов возникают некоторые трудности.
Универсального решения не существует – каждый решает эту проблему по-своему.
Возможно, поможет какая-то система регистрации мутантов.
Мы в Badoo сейчас думаем о чем-то подобном, отключим их.
Это теория.
А что насчет PHP? Есть два хорошо известных инструмента для тестирования мутаций: обман и инфекция.
Когда я готовил статью, мне хотелось поговорить о том, какой из них лучше, и прийти к выводу, что это Заражение.
Но когда я зашел на страницу Humbug, я увидел следующее: Humbug объявил себя устаревшим в пользу Infection. Поэтому часть моей статьи оказалась бессмысленной.
Так что инфекция — действительно хороший инструмент. Я должен сказать спасибо рожден свободным из Минска, который его создал.
Это работает очень круто.
Вы можете взять его прямо из коробки, установить через композитор и запустить.
Нам очень понравилось «Заражение».
Мы хотели его использовать.
Но они не смогли этого сделать по двум причинам.
Заражение требует покрытия кода для правильного и точного запуска тестов на мутантов.
Здесь у нас есть два варианта.
Мы можем вычислить это прямо во время выполнения (но у нас 100 000 модульных тестов).
Либо можем посчитать для текущего мастера (но сборка десяти очень мощных машин в несколько потоков на нашем облаке занимает полтора часа).
Если мы будем делать это при каждом запуске мутации, инструмент, вероятно, не будет работать.
Есть вариант скормить уже готовый, но в формате PHPUnit это куча XML-файлов.
Помимо того, что они содержат ценную информацию, они несут с собой кучу конструкций, какие-то скобки и прочее.
Я подсчитал, что в общем случае наше покрытие кода будет весить около 30 ГБ, и нам нужно будет его таскать по всем облачным машинам и постоянно читать с диска.
В целом идея так себе.
Вторая проблема оказалась еще более существенной.
У нас замечательная библиотека СофтМоккс .
Это позволяет нам иметь дело с устаревшим кодом, который сложно тестировать, и успешно писать для него тесты.
Мы активно его используем и не собираемся отказываться от него в ближайшее время, несмотря на то, что новый код пишем так, что SoftMocks нам не нужны.
Теперь эта библиотека несовместима с Infection, поскольку они используют почти одинаковый подход к мутированию изменений.
Как работают SoftMocks? Они перехватывают включения файлов и заменяют их модифицированными, то есть вместо выполнения класса А SoftMocks создают класс А в другом месте и подключают вместо исходного другой.
Заражение действует точно так же, только действует через поток_wrapper_register() , который делает то же самое, но на системном уровне.
В результате нам могут подойти либо SoftMocks, либо Infection. Поскольку для наших тестов необходимы SoftMocks, подружить эти два инструмента очень сложно.
Наверное, это возможно, но в этом случае мы настолько углубимся в Заражение, что смысл таких изменений просто теряется.
Преодолевая трудности, мы написали свой маленький инструмент. Мы позаимствовали операторы мутации из Infection (они хорошо написаны и очень просты в использовании).
Вместо того, чтобы запускать мутации черезstream_wrapper_register(), мы запускаем их через SoftMocks, то есть используем свой инструмент из коробки.
Наш инструмент совместим с нашей внутренней службой покрытия кода.
То есть он может получать покрытие по требованию для файла или для строки, не запуская всех тестов, что происходит очень быстро.
В то же время это просто.
Если у Infection есть куча разных инструментов и возможностей (например, работа в нескольких потоках), то у нашего ничего подобного нет. Но мы используем нашу внутреннюю инфраструктуру, чтобы смягчить этот недостаток.
Например, мы запускаем одни и те же тесты в несколько потоков через наше облако.
Как мы его используем? Первый – ручной запуск.
Это первое, что нужно сделать.
Все тесты, которые вы пишете, следует проверять вручную с помощью мутационного тестирования.
Это выглядит примерно так:
Я провел тест на мутацию для некоторого файла.
Получил результат: 16 мутантов.
Из них 15 убили тесты, а один упал с ошибкой.
Я не говорил, что мутации могут быть фатальными.
Мы легко можем что-то изменить: сделать недействительным тип возвращаемого значения или что-то еще.
Это возможно, он считается убитым мутантом, потому что наш тест начнет проваливаться.
Однако Infection выделяет этих мутантов в отдельную категорию по той причине, что иногда на ошибки стоит обратить особое внимание.
Бывает, происходит что-то странное и мутанта не совсем корректно считают убитым.
Второе, что мы используем, — это основной отчет. Раз в день, ночью, когда наша инфраструктура разработки простаивает, мы формируем отчет о покрытии кода.
После этого делаем такой же отчет о мутационном тестировании.
Это выглядит так:
Если вы когда-нибудь просматривали отчет о покрытии кода PHPUnit, то наверняка заметили, что интерфейс схож, ведь мы сделали наш инструмент похожим образом.
Он просто рассчитывал все ключевые показатели для конкретного файла в определенном каталоге.
Также мы ставим определенные цели (фактически мы их вытащили из воздуха и пока не следуем им, так как еще не решили, на каких целях ориентироваться по каждой метрике, но они существуют для того, чтобы мы могли легко строить отчеты).
в будущем).
И последнее, самое важное, являющееся следствием двух других.
Программисты — ленивые люди.
Я ленивый: мне нравится, чтобы все работало и не приходилось делать лишних движений.
Мы сделали так, что когда разработчик пушит свою ветку, показатели его ветки и мастера веток автоматически рассчитываются инкрементально.
Например, я закинул два файла и получил следующий результат. В мастере у меня было 548 мутантов, 400 убитых.
По другому файлу 147 против 63. В моей ветке количество мутантов увеличилось в обоих случаях.
Но в первом файле мутанта убили, а во втором он сбежал.
Естественно, оценка MSI упала.
Эта штука позволяет даже людям, которые не хотят тратить время, запускать мутационное тестирование вручную, видеть, что у них получилось хуже, и обращать на это внимание (точно так же, как это делают рецензенты в процессе проверки кода).
Полученные результаты
Пока сложно назвать какие-то цифры: у нас не было никакого показателя, сейчас он появился, но сравнивать его не с чем.Могу сказать, что мутационное тестирование дает психологический эффект. Если вы начинаете прогонять свои тесты через мутационное тестирование, вы невольно начинаете писать более качественные тесты, а написание качественных тестов неизбежно приводит к изменению манеры написания кода — вы начинаете думать, что вам нужно охватить все возможные случаи.
сломанный, вы начинаете его лучше структурировать, более тестируемым.
Это совершенно субъективное мнение.
Но некоторые мои коллеги дали примерно такие же отзывы: когда они стали постоянно использовать мутационное тестирование в своей работе, они стали писать более качественные тесты, и многие говорили, что стали писать более качественный код.
выводы
Покрытие кода — важный показатель, который необходимо отслеживать.Но этот показатель ничего не гарантирует: он не означает, что вы в безопасности.
Мутационное тестирование поможет сделать ваши модульные тесты лучше, а отслеживание покрытия кода — более содержательным.
Инструмент для PHP уже есть, так что если у вас без проблем есть небольшой проект, возьмите его и попробуйте сегодня.
Начните хотя бы с запуска мутационных тестов вручную.
Сделайте этот простой шаг и посмотрите, что он вам даст. Я уверен, тебе понравится.
Теги: #программирование #Тестирование веб-сервисов #Тестирование ИТ-систем #php #мутационное тестирование #юнит-тестирование
-
Приходите И Забирайте Прототипы Книг!
19 Oct, 24 -
Блог-Профсоюз На Страже
19 Oct, 24 -
Отдам В Хорошие Руки...
19 Oct, 24 -
Websocket: Будущее Здесь!
19 Oct, 24 -
Зачем Бить, Если Можно Засосать!?
19 Oct, 24 -
Подкаст «Я Сказал На Каннада» (№3)
19 Oct, 24 -
Хабр Трафик
19 Oct, 24 -
Преимущества Интернет-Опросов
19 Oct, 24 -
Непрерывная Доставка: Продолжение
19 Oct, 24