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

от автора

Мутационное тестирование позволяет выявить баги, которые не покрыты обычными тестами.

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


Комментарии

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

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