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

С юнит-тестами всё более-менее понятно: они должны быть написаны. Иногда они не работают должным образом: существуют ложные срабатывания или тесты с ошибками, которые выдают то «да», то «нет» без каких-либо изменений кода. Небольшие ошибки, которые вы можете обнаружить в модульных тестах, ценны, но часто их фиксит сам разработчик — до того, как он делает коммит. Но нас по-настоящему беспокоят те ошибки, которые часто ускользают из поля зрения. И что хуже всего, они часто решают заявить о себе как раз тогда, когда продукт попадает в руки пользователя.
Именно мутационное тестирование позволяет бороться с такими коварными багами. Оно изменяет исходный код заранее определённым способом (внося специальные баги — так называемые «мутанты») и проверяет, выживут ли эти мутанты в других тестах. Любой мутант, который выжил в модульном тесте, заставляет сделать вывод, что стандартные тесты не обнаружили соответствующий модифицированный фрагмент кода, который содержит ошибку.
В Python основным инструментом мутационного тестирования является mutmut.
Представьте, что нам нужно написать код, который вычисляет угол между часовой и минутной стрелками в аналоговых часах:
def hours_hand(hour, minutes): base = (hour % 12 ) * (360 // 12) correction = int((minutes / 60) * (360 // 12)) return base + correction def minutes_hand(hour, minutes): return minutes * (360 // 60) def between(hour, minutes): return abs(hours_hand(hour, minutes) - minutes_hand(hour, minutes))
Напишем обычный юнит-тест:
import angle def 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.1 rootdir: /home/moshez/src/mut-mut-test collected 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 diff diff --git a/angle.py b/angle.py index 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 diff diff --git a/tests/test_angle.py b/tests/test_angle.py index 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 — выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.
ссылка на оригинал статьи https://habr.com/ru/company/vdsina/blog/512630/

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