Comments 41
имеется публичный метод isLegacy, открытость которого может быть уменьшена
Как эта библиотека за меня определяет публичное апи моего класса? Вообще игры с видимостью внутри юнит тестов выглядят странно. Сделаю я метод protected вместо public и отвалится половина кода, использующего мой клас.
Как говорят умные люди, любой `public` или `protected`метод — это ваш ребенок. Как только вы его написали, вы обязаны следить за ним и за его Backward Compatibility. Особенно, это касается Open Source библиотек, классы которых вы можете наследовать.
Пример из реального проекта: в базовом классе был `protected` метод, который переопределялся в дочерних классах. В результате рефакторинга иерархия классов изменилась, и из базового класса данный метод был удален, оставшись только в одном дочернем. Но модификатор доступа изменить забыли, т.е. он остался `protected`. Мутационное тестирование находит такие проблемы.
Кто-то использовал на реальных проектах? Есть реальные результаты по повышению качества кроме "и спи спокойно"?
Из опенсорса, @Ocramius использует в некоторых библиотеках МТ.
Сразу после прочтения статьи начал искать библиотеки для мутационного тестирования на джава.
Нашёл мейвен плагин Pitest. Несколько тестов "запалились". В халатности, но, к сожалению, есть и ложны срабатывания на Apache ToStringBuilder что он
мутацию в методе toString может заменить на Object.
По поводу "тратить время" — юнит-тесты значительно ускоряют девелопмент, при изменениях дизайна "на лету". Мутационное тестирование просто позволяет удостоверится в том что CodeCoverage действительно такой какой он есть(или меньше)
Спасибо за статью.
Возник вопрос. Насколько я понял, чтобы использовать Infection, код должен быть написан в соответствии с типизацией php7? Или же налачия php-doc с секциями '@param' и '@return' будет достаточно (просто код писался ещё под php5.6, только недавно обновились до php7)?
Насколько я понял, чтобы использовать Infection, код должен быть написан в соответствии с типизацией php7?
Нет, это не обязательно. Просто строгая типизация уменьшает количество сгенерированных мутантов (как в примере про return type).
А так любой код может быть мутирован, с тайпхинтами или нет.
Тут дело не в крутости и сложности исходного кода, а в эффективности юнит тестов. А они то бывают плохими даже для простого кода.
И чем более вольготные мутации, тем сложнее понять следует ли тестам на них реагировать.
Вот, допустим, у нас была функция (простите, питон):
def foo(self):
self.do_job()
self.cleanup()
и приходит мутатор и дописывает в конец self.delete_all. А тесты этого не ловят. Плохие тесты? Или адекватные?
Так как мутация изменяет код, который находится где-то внутри функции, то и unit-тесты (хотя бы один) должны упасть. Если не упали – значит, нет ни одного «внимательного» теста. В любом случае, решение «отрефакторить или нет» всегда точно определено в данном контексте. Или нет?
> замена одного вызова метода класса на другой с похожей сигнатурой
(o.0) Это… заменить `array_map` на `array_filter`? Зачем?
> приходит мутатор и дописывает в конец self.delete_all
А в `cleanup` уже есть `delete_all`? Это ведь уже имеет смысл только в контексте приложения (не языка), а, значит, подобные мутаторы нужно писать самому. Но есть ли в том смысл и резон?
ps: пользуюсь humbug в нескольких проектах, использовать сложные мутации типа «дописать метод» не вижу смысла – время и поддержка тестов не окупятся никогда (имхо).
Про «единичные мутации» — это у меня в голове каша была, мой вопрос примерно был о том, что делается только одна мутация, а не набор мутаций. В принципе, «покрытие всеми возможными вариантами мутаций всех случаев» не требуется, так что вопрос не актуален. Или актуален? Если у нас есть набор точек для мутации и набор мутаций, то надо ли пробовать пермутацию мутаций (т.е. комбинации из двух разных мутаций, из трёх и т.д.)? Я вижу комбинаторный взрыв в этом.
P. S.: Для PHP сложно представить проекты, для которых такое окупится, мне кажется.
> надо ли пробовать пермутацию мутаций
Не нужно, т.к. не имеет смысла – комбинация мутаций провалит те же самые тесты, что и отдельные мутации сами по себе, – они уже помогают резко улучшить качество тестов и приносят профит за копейки (оставить проект проверяться на обед/ночь – не сложно).
Комбинация может найти баги только если если покрытие кода не плотное или невнимательное.
Может быть так, что единичная мутация приводит к убитому мутанту, но вторая мутация помогает ему прорваться через тест.
Конечно, полное покрытие бессмысленно/невозможно/дорого. Есть подозрение, что десятилетия процессорного времени на «автоматически мутационный брутфорс» не окупятся при такой критичности багов – проще/быстрее/дешевле нанять специально обученных хороших тестеров, которые приложат свои биологические нейронные сети для поиска (-: Но нужно считать, конечно.
Вопрос в воздух: интересно, используют ли такое в авиационном или космическом софте?
Например, если у нас мутант заменяет "-1" на "+1", вот этот код свалится с ошибкой:
save_something[index + 1]
return get_something[index + 1]
Первый мутант свалится потому, что мы возвращем не то, что сохранили. А двойная мутация проверяет, что мы ловим ошибку «записали не туда». То есть наш код работает, а соседу данные попорчены.
Вопрос нагрузки на процессор я не знаю. У меня есть приложение с ~500 тестами (юнит и интеграционные с кассетами, плюс несколько реальных тестов со sleep'ами). Тесты отрабатывают за 25с. В приложении ~2700 строк. Часть из них — json schema, так что реальный размер — примерно 2500. Если предположить, что у нас в среднем одна мутация на строку, то полный прогон мутантов первого ранга (при условных 30 мутациях) — это 520 часов полного прогона.
Для мутантов второго ранга это уже 1627604 суток машинного времени. Даже в режиме «богатые дяди»ферма из сотни серверов с 32 ядрами каждый, уменьшит это до «всего лишь» 500 суток.
Чисто для fun, у этих же богатых дядь мутанты третьего ранга займут 10 миллионов лет.
Мдя, не смешно. Явно надо учиться резать ветвления.
Я не описал этот момент в статье, но у мутационного тестирования есть свои методики увеличения скорости работы.
Самая основная, это запуск *только тех тестов, которые покрывают мутируемую строку*.
Это означает, что для Мутации X вам потребуется запуск не 500 тестов, а только Y из них (1,2, 5 — сколько получится). Более того, следующий шаг, это запуск сначала самых быстрых тестов, из тех что покрывают строку.
В целом, это кардинально снижает время, затраченное на мутационное тестирование.
Не знаю, может стоит написать Часть 2 про разные методики улучшения производительности, и как МТ устроено внутри?
С мутационным тестированием на самом деле интересны две вещи: getting started для проекта с существующими тестами, и best practices.
Кстати, юнит-тесты без выхода на сайд-эффекты часто вообще смысла не имеют (а если имеют — то это уже не юнит-тесты). Я недавно про это писал (английский): medium.com/python-pandemonium/mock-or-not-to-mock-41965d33f175
оставить проект проверяться на обед/ночь – не сложно
У Java, Pitest умеет делать инкрементальное мутационное тестирование. Так что, это даже "Оставь на одну ночь, потом запускай хоть каждые 15 минут"
Буду благодарен за любые полезные комментарии по этому поводу.
tl;dr: Infection генерирует больше мутантов, но «лишних» – количество выживших одинаковое. Кэширование в Humbug даёт лучшее ускорение, чем параллелизация в Infection.
В итоге, я не вижу для себя смысла переходить. Хотя, судя по статье, Infection понравился больше.
- Исходные данные: средний проект, покрываться тестами начал уже после релиза в прод, новый код пишется после тестов (в основном юниты, но есть и «юзкейс-тесты», которые дёргают базу). Всего тестов 460, 2344 assertions, проходят за полторы минуты полностью.
- Запуск. Infection сразу не завёлся. В Humbug я ставил таймаут 10 секунд. Infection начал работать только когда я выставил таймаут две минуты – время, большее чем время прохождение одного теста. Почему – не понял, но это не очевидно.
- Отчёты. Humbug сохраняет в лог ещё и остальные метрики вроде MSI/MCS/покрытия, генерации и ресурсов. Infection – только диффы. Так что перед написанием коммента пришлось ещё раз полностью запустить Infection.
- Скорость. Без ускорений Humbug быстрее: 1,15 часа против почти двух (из-за меньшего количества мутантов?). Без тестов трогающих базу, с кэшированием и параллелизацией Humbug опять же быстрее: 3-5 минут в среднем против 20 минут.
- Мутанты. Infection сгенерировал на 30% больше мутантов (2360 против 1682), но количество выживших осталось ровно таким же (156). Эти «лишние» мутанты размазались между «убитыми» и «не покрыты тестами». MSI/MCS/Covered различаются на 1-2%.
mutpy, mutmut и cosmic-ray. Ни один из них не пакетирован в дебиане, к сожалению.
Постоянно, вряд ли, буду прогонять с тестами, а вот в момент написание основных тестов пригодится, чтобы улучшить их покрытие и качество.
Мутационное тестирование