Как стать автором
Обновить

Комментарии 41

имеется публичный метод isLegacy, открытость которого может быть уменьшена

Как эта библиотека за меня определяет публичное апи моего класса? Вообще игры с видимостью внутри юнит тестов выглядят странно. Сделаю я метод protected вместо public и отвалится половина кода, использующего мой клас.
За вас она ничего не определяет, она пытается понизить модификатор доступа метода, и, если при этом тесты не падают, значит есть вероятность, что метод зря имеет такой уровень доступа. Такую мутацию надо проанализировать и либо дописать тест, либо сделать соответствующие изменения в коде.

Как говорят умные люди, любой `public` или `protected`метод — это ваш ребенок. Как только вы его написали, вы обязаны следить за ним и за его Backward Compatibility. Особенно, это касается Open Source библиотек, классы которых вы можете наследовать.

Пример из реального проекта: в базовом классе был `protected` метод, который переопределялся в дочерних классах. В результате рефакторинга иерархия классов изменилась, и из базового класса данный метод был удален, оставшись только в одном дочернем. Но модификатор доступа изменить забыли, т.е. он остался `protected`. Мутационное тестирование находит такие проблемы.

Кто-то использовал на реальных проектах? Есть реальные результаты по повышению качества кроме "и спи спокойно"?

Из того что я знаю: у нас на работе ребята на митапе рассказывали, что используют на многих новых проектах, причем выставляя на билдах уровень в 100%. Но там Ruby и МФ — Mutant, возможно, он позволяет регистрировать false-positives и упомянутые в статье идентичный мутации.

Из опенсорса, @Ocramius использует в некоторых библиотеках МТ.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Сразу после прочтения статьи начал искать библиотеки для мутационного тестирования на джава.
Нашёл мейвен плагин Pitest. Несколько тестов "запалились". В халатности, но, к сожалению, есть и ложны срабатывания на Apache ToStringBuilder что он
мутацию в методе toString может заменить на Object.


По поводу "тратить время" — юнит-тесты значительно ускоряют девелопмент, при изменениях дизайна "на лету". Мутационное тестирование просто позволяет удостоверится в том что CodeCoverage действительно такой какой он есть(или меньше)

Спасибо

НЛО прилетело и опубликовало эту надпись здесь
Хорошая идея, подумаю о добавлении этой фичи :)
Пока это невозможно, т.к. надо хранить где-то значения.
Давно думал, что процент покрытия тестами не говорит о качестве этого покрытия.
Спасибо за статью.
Возник вопрос. Насколько я понял, чтобы использовать Infection, код должен быть написан в соответствии с типизацией php7? Или же налачия php-doc с секциями '@param' и '@return' будет достаточно (просто код писался ещё под php5.6, только недавно обновились до php7)?
Насколько я понял, чтобы использовать Infection, код должен быть написан в соответствии с типизацией php7?


Нет, это не обязательно. Просто строгая типизация уменьшает количество сгенерированных мутантов (как в примере про return type).

А так любой код может быть мутирован, с тайпхинтами или нет.
Ага. Ну то есть если у нас тест сломался при подмене на return null, то всё хорошо, а если не сломался, то с тестами что-то не то. Просто при наличии строгой типизации наличие такого мутанта (как в return type) бессмысленно. Теперь вроде ясно. Спасибо.
Забавно будет применить этот термин к ДНК-компьютерам
Думаю что и такие тесты и fuzzing применимы там, где пишется код который многократно переиспользуется. Для библиотек, ядерного функционала. Ну или просто сложного функционала с повышенными требованиями к качеству. Для обычного повседневного кода не факт что это нужно. Но техника интересная, стоит попробовать.
Вы удивитесь, попробуя даже для вашего pet-project мутационное тестирование.
Тут дело не в крутости и сложности исходного кода, а в эффективности юнит тестов. А они то бывают плохими даже для простого кода.
Обычно в пет проджектах я тестами покрываю только то что неудобно тестировать иным способом (в т.ч. ручным) и од состояния мне хватило уверенности, т.е. там покрытие реально 5-10 % кода обычно. Хотя опять же зависит. Весь код который всякие ui делает и простую бизнес логику я обычно не тестирую вообще в пет проектах
Выглядит крайне интересно, хотя у меня возникает вопрос о типе вносимых мутаций. Если мы мутируем только единичные операции, то область «проверки» очень ограничена (большинство других «невнимательных тестов» не будут пойманы). Если мы начинаем использовать широкие возможности мутаций (например, замена одного вызова метода класса на другой с похожей сигнатурой), то процесс генерации мутации становится уже трудноописуемым.

И чем более вольготные мутации, тем сложнее понять следует ли тестам на них реагировать.

Вот, допустим, у нас была функция (простите, питон):
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 в нескольких проектах, использовать сложные мутации типа «дописать метод» не вижу смысла – время и поддержка тестов не окупятся никогда (имхо).
Я посмотрел видео про cosmic rays (https://www.youtube.com/watch?v=jwB3Nn4hR1o), там перечислены типы мутаций. Да, оно более строго, чем я предполагал.

Про «единичные мутации» — это у меня в голове каша была, мой вопрос примерно был о том, что делается только одна мутация, а не набор мутаций. В принципе, «покрытие всеми возможными вариантами мутаций всех случаев» не требуется, так что вопрос не актуален. Или актуален? Если у нас есть набор точек для мутации и набор мутаций, то надо ли пробовать пермутацию мутаций (т.е. комбинации из двух разных мутаций, из трёх и т.д.)? Я вижу комбинаторный взрыв в этом.
Идея с комбинациями мутаций интересная, но это правда слишком сложно получается. Такое может окупиться только на каких-то очень ответственных участках кода, т. к. на возню с тестами и мутациями будет времени уходить в разы больше, чем на сам код.

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 про разные методики улучшения производительности, и как МТ устроено внутри?
Когда вы тестируете только функции, которые мутировались, вы автоматически исключаете «ловлю» сайд-эффектов. С точки зрения «прочности тестов» юнит-тесты ловят только логику. А представьте себе, что у вас в результате мутации file(filename,O_READ) стало file(filename, O_WRITE). Сайд-эффекты от удалённого файла будут заметны только в другом коде.

С мутационным тестированием на самом деле интересны две вещи: getting started для проекта с существующими тестами, и best practices.
НЛО прилетело и опубликовало эту надпись здесь
Тыг мутации-то применимы не только к юнит-тестам, но и к любым другим тестам.

Кстати, юнит-тесты без выхода на сайд-эффекты часто вообще смысла не имеют (а если имеют — то это уже не юнит-тесты). Я недавно про это писал (английский): medium.com/python-pandemonium/mock-or-not-to-mock-41965d33f175
НЛО прилетело и опубликовало эту надпись здесь
оставить проект проверяться на обед/ночь – не сложно

У Java, Pitest умеет делать инкрементальное мутационное тестирование. Так что, это даже "Оставь на одну ночь, потом запускай хоть каждые 15 минут"

Скажите, пробовали ли вы Infection там, где пробовали Humbug? Интересна разница на «живых примерах».

Буду благодарен за любые полезные комментарии по этому поводу.
Про Infection я узнал только из этой статьи так что полезных данных нет пока что. Но выглядит он интересно, так что в скором времени опробую и сравню (не уверен что на этой неделе), напишу.
Опробовал, сравнил. Пробовал на трёх проектах, но результаты сравнимы поэтому в качестве примера приведу только один.

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. Ни один из них не пакетирован в дебиане, к сожалению.
Мутационное тестирование — это интересно и сама утилита отличная, спасибо вам за нее. Запустил на проекте и нашел несколько мест для улучшения набора данных в тестах.

Постоянно, вряд ли, буду прогонять с тестами, а вот в момент написание основных тестов пригодится, чтобы улучшить их покрытие и качество.
А сколько у вас занимает МТ для вашего проекта?
Могли бы добавить в билд с опцией `--min-covered-msi`.

Данный показатель позволит не снижать качество существующих и новых тестов. Но если писать новые не будете, билд падать не будет.
Проект маленький, несколько секунд. Подумаю над этим, спасибо)
НЛО прилетело и опубликовало эту надпись здесь
Из вашего комментария не совсем понял, хорошо это или плохо?

Но а целом да, не упоминал специально.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории