python

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

  • вторник, 28 июля 2020 г. в 00:27:06
https://habr.com/ru/company/vdsina/blog/512630/
  • Блог компании VDSina.ru — хостинг серверов
  • 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 — выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.