У вас есть тесты на все случаи жизни? Или может быть, в репозитории вашего проекта даже лежит справка О 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 выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.