Хабрахабр

Мутационный анализ, или как тестировать тесты

Тестов много не бывает — это все знают. Мемы про unit и интеграционное тестирование уже не очень-то веселят. А мы по-прежнему не знаем, можно ли полагаться на результаты прохождения тестов, и какой процент покрытия позволит не пустить баги в продакшен. Если фатальные изменения в коде проскакивают тесты, не оказывая влияния на их результат, то решение напрашивается само — надо тестировать тесты!

Видео и статья короткие, а идеи очень рабочие — надо брать на заметку.
О подходе к автоматизации этой задачи и был доклад Марка Лангового на Frontend Conf.

О спикере: Марк Ланговой (marklangovoi) работает в Яндексе в проекте Яндекс.Толока. Это краудсорсинговая площадка для быстрой разметки большого количества данных. Заказчики загружают данные, которые, например, нужно подготовить для использования в алгоритмах машинного обучения, и назначают цену, а другая сторона — исполнители могут выполнять задания и зарабатывать.

В свободное от работы время Марк развивает Краснодарское сообщество разработчиков Krasnodar Dev Days — одно из 19 IT-сообществ, активистов которых мы пригласили на Frontend Conf в Москву.

Тестирование

Существуют разные виды автоматизированного тестирования.

Они просты в написании, но иногда во время интеграции с другими модулями могут вести себя не совсем так, как мы ожидали. В ходе популярного модульного тестирования мы пишем тесты на маленькие части (модули) приложения.

Чтобы избежать этого, мы можем писать интеграционные тесты, которые будут проверять работу наших модулей совместно.

Они немного сложнее, поэтому сегодня мы остановимся на модульном тестировании.

Модульное тестирование

Любой проект, который хочет хоть какой-то минимальной стабильности, занимается написанием модульных тестов.

Рассмотрим пример.

class Signal off(callback) { const callbackIndex = this.listeners.indexOf(callback); if (callbackIndex === -1) { return; } this.listeners = [ ...this.listeners.slice(0, callbackIndex - 1), ...this.listeners.slice(callbackIndex) ]; } trigger() { ... }
}

Есть класс Signal — это Event Emitter, у которого есть метод on для подписки и метод off для удаления подписки — проверяем, если callback содержится в массиве подписчиков, то удаляем. И, конечно, есть метод trigger, который будет вызывать подписанные callback.

У нас есть простой тест для этого примера, который вызывает методы on и off, а затем trigger, для того чтобы проверить, что callback не вызвался после отписки.

test(’off method should remove listener', () => { const signal = new Signal(); let wasCalled = false; const callback = () => { wasCalled = true; }; signal.on(callback); signal.off(callback); signal.trigger(); expect(wasCalled).toBeFalsy();
});

Критерии оценки качества

Какие есть критерии оценки качество такого теста?

Code coverage — самый популярный и всем известный критерий, который показывает, сколько процентов строк кода были исполнены при запуске теста.

У вас может быть 70%, 80% или все 90% Code coverage, но значит ли это, что, когда вы соберете очередной билд для продакшена, все будет хорошо, или что-то может пойти не так?

Вернемся к нашему примеру.

И тут натыкаешься на этот код, который написал твой коллега. Вечер пятницы, ты устал, заканчиваешь очередную фичу. Тебе что-то в нем показалось сложным и страшным.

...this.listeners.slice(0, callbackIndex - 1), ...this.listeners.slice(callbackIndex)

Ты решил, что наверное можно просто очищать массив:

class Signal { ... off(callback) { const callbackIndex = this.listeners.indexOf(callback); if (callbackIndex === -1) { return; } this.listeners = []; } ...
}

Сделал коммит, собрал проект и отправил в продакшен. Тесты прошли — почему бы и нет? И пошел отдыхать в бар.

Ты горишь, тебе грозит увольнение. Но вдруг поздно ночью звонок, в трубку кричат, что все падает, люди не могу пользоваться продуктом, и вообще — бизнес теряет деньги!

Что делать с тестами? Как с этим быть? Кто же будет тестировать тесты? Как отлавливать такие примитивные глупые ошибки?

Конечно, можно нанять армию QA-инженеров — пусть сидят и просто клацают наше приложение.

На них можно свалить работу по написанию тестов — зачем писать самим, если для этого есть специальные люди? Или нанять QA-автоматизаторов.

Но на самом деле это дорого, поэтому мы сегодня поговорим про мутационный анализ или мутационное тестирование.

Мутационное тестирование

Это способ автоматизировать процесс тестирования наших тестов. Его цель — выявление неэффективных и неполных тестов, то есть, по сути, это тестирование тестов.

Идея состоит в том, чтобы изменять куски кода, запускать на них тесты, и, если тесты не упали, значит, они неполные.

Они заменяют, например, плюс на минус, умножить на разделить, и прочие подобные операции. Изменения вносятся с помощью определенных операций — мутаторов. Мутаторы могут менять куски кода, заменять условия в while, обнулять массивы вместо добавления в массив какого-то элемента.

В результате применения мутаций на исходный код, он мутирует и становится мутантом.

Мутанты делятся на две категории:

  1. Убитые — те, в которых мы смогли выявить отклонения, то есть на которых хотя бы один тест упал.
  2. Выжившие — те самые, которые убежали от нас, и донесли баг до продакшена.

Для оценки качества есть метрика MSI (Mutation Score Indicator) — процентное отношение между убитыми и выжившими мутантами. Чем больше разница между покрытием кода тестами и MSI, тем хуже отражает актуальность наших тестов процент покрытия кода.

Это было немножко теории, а теперь рассмотрим, как это можно использоваться в JavaScript.

Решение для JavaScript

В JavaScript существует только один активно развивающийся инструмент мутационного тестирования — это Stryker. Такое название инструмент получил в честь персонажа X-man Уильяма Страйкера — создателя «Оружия X» и борца со всеми мутантами.

Это framework для мутационного тестирования, который дополняет вашу текущую инфраструктуру. Stryker не является test runner, как Karma или Jest; также он не является framework’ом для тестов, как Mocha или Jasmine.

Система плагинов

Stryker очень гибкий, полностью построен на системе плагинов, большинство из которых написаны разработчиками Stryker’a.

Есть интеграция с фреймворками Mocha (stryker-mocha-framework) Jasmine (stryker-jasmine) и готовые наборы мутаторов для JavaScript, TypeScript и даже для Vue: Существуют плагины для запуска тестов на Jest, Karma и Mocha.

  • stryker-javascript-mutator;
  • stryker-typescript;
  • stryker-vue-mutator.

Мутаторы для React входят в stryker-javascript-mutator. Помимо этого, вы всегда можете написать свои мутаторы.

Если код нужно преобразовать перед запуском, можно использовать плагины для Webpack, Babel или TypeScript.

Настраивается это все относительно просто.

Конфигурация

Конфигурирование не составит большого труда: вам только нужно указать в JSON-конфиге, какой test runner (и/или test framework, и/или transpiler) вы используете, а также установить соответствующие плагины из npm.

Она спросит вас, что вы используете, и сформирует конфигурацию самостоятельно. Простая консольная утилита stryker-cli может все это сделать за вас в режиме вопрос-ответ.

Как это работает

Жизненный цикл прост и состоит из следующих шагов:

  • Чтение и анализ конфига. Stryker загружает конфиг и анализирует его на различные плагины, настройки, исключение файлов и т.д.
  • Загрузка плагинов согласно конфигу.
  • Запуск тестов на исходном коде для того, чтобы проверить, актуальны ли сейчас тесты (вдруг они уже сломаны).
  • Если все хорошо, генерируется набор мутантов по файлам, которые мы разрешили мутировать.
  • Запуск тестов на мутантах.

Выше пример запуска Stryker:

  • Stryker запускается;
  • считывает конфиг;
  • подгружает нужные зависимости;
  • находит файлы, которые будет мутировать;
  • запускает тесты на исходном коде;
  • создает 152 мутанта;
  • запускает тесты в 8 потоков (в данном случае на базе количества ядер CPU).

Это все не быстрый процесс, поэтому лучше его делать на каких-нибудь CI/CD серверах.

После прохождения всех тестов Stryker дает краткий отчет по файлам с количеством созданных, убитых и выживших мутантов, а также процент соотношения убитых мутантов к выжившим (MSI) и мутаторы, которые были применены.

Это и есть потенциально возможные проблемы, которые не предусмотрели в наших тестах.

Подытожим

Мутационное тестирование полезно и интересно. Оно может найти проблемы на ранних стадиях тестирования, причем без участия людей. Оно сократит время на проверку pull request, например, за счет того, что квалифицированным разработчикам не нужно будет тратить время на проверку pull request, в котором уже есть потенциальные проблемы. Или спасет продакшен, если вы решите подготовить новый релиз в пятницу вечером.

Он активно развивается, но пока сыроват, до сих пор не дошел до мажорной версии. Stryker — гибкий многопоточный инструмент для мутационного тестирования. Это OpenSource проект, которому можно помочь развиваться. Например, за период подготовки этого доклада, его разработчики наконец сделали в плагине для Babel возможность указать файл конфигурации и починили Jest-интеграцию.

Вопросы-ответы

— Как тестировать мутационные тесты? Наверняка, тоже есть погрешность. В первом примере с модульным тестированием было покрытие 90%. Казалось бы, все хорошо, но все равно проскакивали кейсы, когда все падало и было в огне. Соответственно, почему должно появиться ощущение того, что все хорошо, после покрытия еще этих тестов мутационными?

Естественно, могут быть какие-то пограничные безумные случаи или отсутствие какого-то мутатора. — Я не говорю, что мутационное тестирование — это серебряная пуля и все вылечит. Например, ставишь проверку на возраст, поставил ее <18 (нужно было <=), а в тесте забыл сделать проверку пограничного случая. В первую очередь легко отлавливаются типичные ошибки. Такие вещи быстро отлавливаются. У тебя выполнилось другое сравнение мутатором, и в итоге тест упал (или не упал), и ты понимаешь, что все хорошо или все плохо. Это способ просто дописать тесты правильно, найти упущенные моменты.

Я считаю, что это неверно. — Часто у тебя происходит ситуация «задеплоил и ушел»?

Естественно, это неверно. — Нет, но я думаю, что в многих проектах подобные вещи все-таки существуют. Многие считают, что Code coverage помогает все проверить, можно спокойно уйти и не переживать — но это не так.

У нас куча всяких редьюсеров и прочего, что мы мутационно тестируем, и их очень много. — Сразу скажу, в чем проблема. Есть ли возможность запуска только на то, что изменилось? Это все разрастается, и получается, что на каждый pull request запускается мутационное тестирование, которое занимает много времени.

Например, на стороне разработчика, когда он пушит, комитит, можно сделать lint-staged плагин, который будет прогонять только те файлы, которые изменились. — Думаю, это можно настроить самому. В нашем случае проект очень большой и старый, и мы практикуем точечную проверку. На CI/CD тоже такое возможно. Я бы рекомендовал делать точечные проверки, либо самому организовывать выборочный процесс запуска. Мы не проверяем все, потому что это займет неделю, будут сотни тысяч мутаций. Готового инструмента для такой интеграции я не видел.

Если нет, то, как именно выбираются мутации? — Обеспечивается ли полнота всех возможных мутаций для конкретного фрагмента кода?

Stryker должен генерировать все возможные мутации на один и тот же фрагмент кода. — Лично не проверял, но и проблем с этим не встречал.

У меня unit-тест тестирует и логику, и, в том числе, верстку snapshot react-компонента. — Хочу спросить по поводу snapshot’ов. Это ожидаемое поведение, разве не так? Естественно, если я любую логическую конструкцию изменю, у меня тут же поменяется верстка.

— Да, в этом их смысл, что ты сам вручную snapshot’ы обновляешь.

— То есть ты snapshot’ы как-то игнорируешь в этом репорте?

— Скорее всего, snapshot’ы нужно заранее обновить, а потом запустить мутационное тестирование, иначе будет куча мусора от Stryker.

Для просто unit-тестов есть reporter’ы — под GitLab, под все, что угодно, которые выводят процент успешного прохождения тестов, и ты можешь настроить — фейлить или не фейлить. — Вопрос по поводу CI-серверов. Он просто выводит табличку в консоль, но что дальше с ней делать? А что у Stryker?

Возможно, есть какие-то конкретные инструменты, но так как мы пока занимаемся точечным мутационным тестированием, я не находил конкретных интеграций с TeamCity и подобными инструментами CI/CD. — У них есть HTML-reporter, можно сделать свои reporter’ы — все гибко настраивается.

То есть тесты — это боль, и тесты надо переписывать, когда код переписывается, и пр. — Насколько мутационные тесты увеличивают поддержку вообще тестов, которые у тебя есть? А тут я еще и мутационные тесты. Иногда проще код переписать, чем тесты. Насколько это дорого для бизнеса?

Код должен быть легко тестируемым. — Сначала я, наверное, поправлю, что переписывать код ради тестов — это неправильно. Если они не полные, это значит, что может возникнуть баг, который принесет потери. Насчет того, что нужно дописывать — это опять же для бизнеса важно, чтобы тесты были максимально полны и эффективны. Естественно, можно тестировать только самые важные для бизнеса части.

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

Если сейчас тесты написаны плохо, то придется много дописывать. — Это дороже настолько, насколько плохие тесты сейчас. Мутационное тестирование будет находить случаи, которые не покрыты тестами.

Как обрабатывать ложные срабатывания? — На слайде с результатами проверки Stryker много ворнингов, они критические или не критические.

Я спрашивал ребят у нас в команде, что у них выпадало интересного из таких ошибок. — Тонкий вопрос — что считать ложным. Stryker выдал, что тесты не отреагировали на то, что текст ошибки изменился. Было пример насчет текста ошибки. Вроде бы, косяк, но минорный.

— То есть вы такие ошибки видите и пропускаете некритичные в ручном режиме?

— У нас точечная проверка, поэтому да.

Когда вы это внедрили, какой процент тестов у вас повалился? — У меня практический вопрос.

Поэтому не могу сказать точных цифр, но в целом подход однозначно улучшил ситуацию.
— Мы не внедряли на всем проекте, но на новом проекте находились мелкие проблемы.

Или подпишитесь на рассылку, и мы будем держать вас в курсе всех новых материалов и новостей будущих конференций. Другие, такие же полезные, выступления по фронтенду смотрите на нашем youtube-канале, туда постепенно попадают все тематические доклады со всех наших конференций.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»