Русский
Русский
English
Статистика
Реклама

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

Перевод Изучаем mutmut инструмент для мутационного тестирования на Python

27.07.2020 14:05:23 | Автор: admin
Мутационное тестирование позволяет выявить баги, которые не покрыты обычными тестами.

У вас есть тесты на все случаи жизни? Или может быть, в репозитории вашего проекта даже лежит справка О 100-процентном тестовом покрытии? Но разве в реальной жизни всё так просто и достижимо?




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

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

В Python основным инструментом мутационного тестирования является mutmut.

Представьте, что нам нужно написать код, который вычисляет угол между часовой и минутной стрелками в аналоговых часах:

def hours_hand(hour, minutes):    base = (hour % 12 ) * (360 // 12)    correction = int((minutes / 60) * (360 // 12))    return base + correctiondef minutes_hand(hour, minutes):    return minutes * (360 // 60)def between(hour, minutes):    return abs(hours_hand(hour, minutes) - minutes_hand(hour, minutes))


Напишем обычный юнит-тест:

import angledef test_twelve():    assert angle.between(12, 00) == 0


В коде нет ни одного if. Давайте проверим, насколько покрывает все возможные ситуации такой юнит-тест:

$ coverage run `which pytest`============================= test session starts ==============================platform linux -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1rootdir: /home/moshez/src/mut-mut-testcollected 1 item                                                              tests/test_angle.py .                                                    [100%]============================== 1 passed in 0.01s ===============================


Отлично! Вроде бы 100-процентное покрытие. Но что произойдёт, когда мы проведём мутационное тестирование?



О нет! Из 21 выжили целых 16 мутантов. Как же так?

Для каждого мутационного теста нужно изменить часть исходного кода, которая имитирует потенциальную ошибку. Примером такой модификации является изменение оператора сравнения > на > =. Если для этого граничного условия нет модульного теста, этот баг-мутант выживет: это потенциальная ошибка, которую не обнаружит ни один из обычных тестов.

Ну ладно. Всё ясно. Надо писать юнит-тесты лучше. Тогда с помощью команды results посмотрим, какие конкретно изменения были сделаны:

$ mutmut results<snip>Survived :( (16)---- angle.py (16) ----4-7, 9-14, 16-21$ mutmut apply 4$ git diffdiff --git a/angle.py b/angle.pyindex b5dca41..3939353 100644--- a/angle.py+++ b/angle.py@@ -1,6 +1,6 @@ def hours_hand(hour, minutes):     hour = hour % 12-    base = hour * (360 // 12)+    base = hour / (360 // 12)     correction = int((minutes / 60) * (360 // 12))     return base + correction


Это типичный пример работы mumut: он анализирует исходный код и заменяет одни операторы на другие: например, сложение на вычитание или, как в данном случае, умножение на деление. Модульные тесты, вообще говоря, должны выявлять ошибки при смене оператора; в противном случае они не проверяют поведение программы эффективно. Этой логики mutmut как раз и придерживается, внося те или иные изменения.

Мы можем использовать команду mutmut apply (применить мутацию к нашему коду) для выжившего мутанта. Ничего себе: оказывается, мы не проверили, правильно ли использовался параметр час (hour). Исправим это:

$ git diffdiff --git a/tests/test_angle.py b/tests/test_angle.pyindex f51d43a..1a2e4df 100644--- a/tests/test_angle.py+++ b/tests/test_angle.py@@ -2,3 +2,6 @@ import angle  def test_twelve():     assert angle.between(12, 00) == 0++def test_three():+    assert angle.between(3, 00) == 90


Раньше мы проверяли только для 12. Добавление проверки для значения 3 спасёт ситуацию?



Этот новый тест сумел убить двух мутантов: это лучше, чем раньше, но нужно поработать ещё. Я не буду сейчас писать решение для каждого из 14 оставшихся случаев, потому что идея и так понятна (можете ли вы убить всех мутантов самостоятельно?)

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



На правах рекламы


VDSina предлагает виртуальные серверы на Linux и Windows выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.

Подробнее..

Mutation Driven Development

05.02.2021 22:21:47 | Автор: admin

Статья написана на основе поста в telegram-канале Cross Join.


Прежде чем рассказать об этом подходе сначала в двух словах объясню, что такое мутационное тестирование вообще. Для тех, кто не в курсе.


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


Когда вы пишете тесты, по TDD или нет, даже с формальным 100% покрытием, вы никогда не будете уверены в том, что в коде на самом деле протестировано всё. Например, можно банально ошибиться в вызове assert в самом тесте.


assertEquals($a, $a);

И если даже при тестировании удалось дойти до какого-то if, не факт, что в этом if правильно проверены все условия.


Чтобы убедиться в том, что тесты реально тестируют, есть такой способ: самому вносить в код баги, и смотреть, валятся ли от этого тесты. Если тесты по-прежнему зелёные при явном баге, значит протестировано не всё.


Например, заменили плюс на минус, или добавили "не" к условию, и смотрите, отреагировали ли тесты. Такой подход называется "Мутационное тестирование".


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


Конечно в марсоходостроении это все делается автоматически: специальный инструмент уродует вашу программу и смотрит, слетели ли тесты.


Проблема в этом подходе в том, что это довольно муторно настраивается, много нюансов, как описать, что надо мутировать, что нет. И всё это потребляет дикое количество ресурсов: разбор кода в AST, мутирование и обратное преобразование.


Mutation Driven Development


Сейчас некоторые авторы предлагают подход mutation driven development. Он аналогичен TDD, но как бы наоборот.


1) сначала добавляете новый код в проект


2) временно вносите баг


3) пишете тест, который ловит этот баг (или фиксите старые тесты)


4) переходите к следующей строке (или к следующей части условия if). Конечно, и здесь можно случайно пропустить кусок логики, но в целом MDD (?) гораздо более упорот, чем 100% сoverage или TDD.


Для меня такой подход видится интересным тем, что можно ничего не настраивать в CI, не просить выделить доп ресурсы для обработки пайплайнов, а просто писать более покрытый код, даже если тимлиду или вообще никому в твоей команде в целом MDD неинтересен.


Конечно, это не бесплатно, и нещадно пожирает ресурсы программиста. Имхо mutation driven testing можно и нужно применять только там, где есть ответственная логика. Работа с деньгами, медицинский софт и т.д. Понятно, что на эндпоинт, выдающий текущую погоду или прочую ерунду, можно не тратить ресурсы, как ручные, так и автоматические.Вообще, множество проектов содержат 95% стандартного бойлерплейта, и лишь чуть-чуть важной бизнес-логики. Вот эту важную логику при желании можно довести до состояния, близкого к идеалу.


Конечно же мы обсудим mutation driven development в одном из ближайших выпусков "Цинкового прода", не забудьте подписаться на подкаст

Подробнее..

Я сомневался в юнит-тестах, но

13.10.2020 14:23:08 | Автор: admin
Когда я пишу тест, то часто не уверен, что мой дизайн будет на 100% удачным. И хочу, чтобы он давал гибкость в рефакторинге кода например, чтобы затем изменить класс, не меняя код теста.



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

Всем привет! Это расшифровка подкаста Между скобок моих интервью с интересными людьми из мира разработки на PHP.


Запись, если вам удобнее слушать. В полной аудиоверсии мы также обсуждаем больше вопросов code coverage.

С Владимиром vyants Янцем мы познакомились на февральском PHP-митапе в Ростове: я рассказывал про свой опыт с асинхронностью, он делал доклад про тесты. С того выступления у меня остались вопросы и в период карантина мы созвонились, чтобы обсудить их.

Сергей Жук, Skyeng: Начнем с главного. Нужны ли юнит-тесты? Не рискую ли я получить слишком большую связность теста и кода? Да и проект наверняка будет меняться, почему бы мне не пойти от вершины пирамиды тестирования, с тех же функциональных тестов?

Владимир Янц, Badoo: Это очень хороший вопрос. Давай начнем с того, нужны ли они в принципе. Может, и правда, только функциональные?

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

Например, у нас на данный момент 20 000 приемочных функциональных тестов. Если запустить их в один поток, они будут бежать несколько дней. Соответственно, нам пришлось придумывать, как получить результаты за минуты: отдельные кластеры, куча машин, большая массивная инфраструктура на поддержку всех этих вещей. А еще ваш тест зависит от окружения: других серверов, того, что лежит в базе данных, кэше. И чтобы его починить, нужно тратить много ресурсов, денег и времени разработчиков.

Чем хорош юнит-тест? Ты можешь протестировать всякие безумные кейсы.


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

Поэтому, отвечая на главный вопрос. Нужны ли хорошие юнит-тесты? Да. Нужны ли плохие? Нет. Один из главных принципов тестирования: тест можно инвестировать в контракт, а не реализацию. Контракт это договоренность, что ваш метод или ваша функция принимает на входе, и что она должна с этим сделать.

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

Сергей Жук, Skyeng: Ок, смотри, у меня какое-то веб-приложение, и я начинаю в нем писать юнит-тесты. Скажем, я знаю, что у этого класса может быть 5 разных input, я меняю реализацию, и просто делаю юнит-тест провайдеру, чтобы не тестить каждый раз. И еще есть какая-то опенсорсная либа тут без юнит-теста тоже никуда. А для каких еще кейсов их нужно и не стоит писать?

Владимир Янц, Badoo: Я бы написал тесты на то, где есть какая-то бизнес-логика: не в базе данных, а именно в PHP-коде. Какие-то хелперы, которые считают что-то и выводят, какие-то бизнес-правила отличные кандидаты для тестирования. Также видел, что пытались тестировать тонкие контроллеры в приложении.

Основное, что должны делать юнит-тесты, спасать чистую функцию.


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

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

Сергей Жук, Skyeng: Когда я начинал, мне казалось, что круто юнит-тестировать максимально детально. Вот у меня есть объект, у него есть какой-то метод и несколько зависимостей (например, модель, которая ходит в базу). Я мокал все. Но со временем понял, что ценность таких тестов нулевая. Они тестируют опечатки в коде и также еще больше связывают тесты с ним. Но я до сих пор общаюсь с теми, кто за такой вот тру-подход. Твоя позиция какова: нужно ли так активно мокать? И в каких кейсах оно того точно стоит?

Владимир Янц, Badoo: В целом, мокать полезно. Но одна из самых вредных конструкций, которых, мне кажется, есть в том же PHPUnit, это ожидания.

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

Мы пытаемся проверить тестом не контракт. Хороший тест этим не занимается.


Это делает тесты очень хрупкими. Вы будете тратить время на фикс, а вероятность поймать баг очень мала. В один момент вам надоест. Я видел такое: ребята начинали писать юнит-тесты, делали это неправильно, и в какой-то момент отказывались от инструмента, потому что он не приносил пользы.

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

Если у вас есть какой-то хэлпер, который складывает в модели два числа, вы должны сделать фейк-объект так, чтобы проверить, что хэлпер сложит числа правильно. Но не нужно проверять, сколько раз он дернул эту модель, сходил ли он в свойства или дернул геттер это детали реализации, которые не должны быть предметом тестирования.

Сергей Жук, Skyeng: Вот ты начал про вред ожиданий. Моки про них. Нужны ли тогда вообще моки, если есть фейки и стабы?

Владимир Янц, Badoo: Проблема стабов в том, что они не всегда удобны: ты должен их заранее создать, описать. Это удобно, когда есть объект, который часто переиспользуется ты один раз его написал, а затем во всех тестах, где нужно, используешь.

А для объекта, который ты применяешь в одном-двух тестах, писать стаб очень долго.


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

Ну а фейки мой любимый подход. Очень помогает тестировать многие вещи. Например, когда ты хочешь протестировать, что все правильно закэшировал. Для Memcached легко создать фейк-объект, он есть в большинстве стандартных поставок фреймворков. В качестве типизации вы используете интерфейс, а не конечный класс, и ваш фейк может быть безопасно передан в качестве аргумента в любую функцию. А внутри, вместо того, чтобы ходить в сам кэш, вы реализуете функционал кэша внутри объекта, делая массив. Это очень здорово облегчает тестирование, потому что вы можете проверить логику с кэшированием иногда это бывает важно.

Сергей Жук, Skyeng: Смотри, еще одна крайность. Я встречал людей, которые говорят: Ок, а как мне протестировать приватный метод/класс?

Владимир Янц, Badoo: Бывает, что не очень правильно структурирован код, и правильный метод может быть отдельной чистой функцией, которая реализовывает функционал. По-хорошему, это должно быть вынесено в отдельный класс, но по каким-то причинам лежит в protected-элементе. В крайнем случае, можно протестировать такое юнит-тестом. Но лучше сделать отдельной сущностью, которая будет иметь свой тест. Что-то вроде своего явного контракта.

Сергей Жук, Skyeng: Вот ты говоришь, есть контракт, а остальное детали реализации. Если мы говорим о веб-приложении с точки зрения интерфейса: у него есть контракт, по которому оно общается с юзерами. В то же время мы формируем реквест, отправляем, инспектируем респонс, сайд-эффекты, БД, еще что-то. Если делать юнит-тесты, то можно вынести логику общения с БД в отдельный слой, завести интерфейс для репозитория, сделать отдельную in memory реализацию репозитория для тестов. Но стоит ли оно того?

Владимир Янц, Badoo: У тебя есть приложение, которое должно правильно работать целиком. И юнит-тесты должны прикрыть какие-то важные вещи, у которых есть большая вариативность ответов, и которые легко выделены в чистую функцию. Вот там они должны быть в первую очередь.

Юнит-тесты могут дать самую быструю обратную связь: запустил, через минуту получил результаты. Если нашелся баг, навряд ли все приложение работает правильно, если что-то не так с его частью.

Умные люди не зря придумали пирамиду тестирования. Юнит-тесты быстрые, их можно написать много. В этом их смысл задешево протестировать очень много участков твоего приложения, чтобы убедиться, что они работают правильно. Чем больше кусочков покрыты юнит-тестами, тем лучше, но это не значит, что надо стремиться к 100-процентному покрытию. Есть много вещей, которые тесты не поймают, потому что не созданы для этого. Но если на их уровне что-то вылезло, нет смысла дальше идти и запускать тяжелые функциональные тесты.

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

Сергей Жук, Skyeng: Давай напоследок поговорим о мутационных тестах. Нужны ли они?

Владимир Янц, Badoo: Нужно внедрять мутационные тесты, как только вы задумались о юнит-тестах. Они поднимут из-под ковра все те проблемы, которые так часто обсуждают бесполезное тестирование, coverage ради coverage.

Там не нужно ничего писать дополнительно. Это просто библиотека: она берет coverage, который у вас где-то есть, идет в исходный код и начнет менять операторы кода на противоположные. После каждой такой мутации она прогоняет тесты, которые, согласно code coverage, эту строчку покрывают. Если ваши тесты не упали, они бесполезны. И, наоборот, можно находить строчки, для которых есть потенциальные мутации, но нет coverage.

Внедрить это на ранних этапах ничего не стоит. А пользы много.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru