** Я перешел от среды. Новый адрес kodare.net **

Я наткнулся на эту статью Хиллела Уэйна об использовании тестирования на основе свойств для поиска ошибки в функции режима (поиск наиболее распространенного элемента). Функция для тестирования такова:

def mode(l):
    max = None
    count = {}
    for x in l:
        if x not in count:
            count[x] = 0
        count[x] += 1
        if not max or count[x] > count[max]:
            max = x
    return max

Первое определение теста на основе свойств в статье:

@given(lists(integers(), min_size=1))
def test_mode(l):
    x = mode(l)
    assert l.count(x) >= l.count(l[0])

Это находит ошибку за довольно короткое время: ~ 3 секунды, когда я пытался. Не так плохо! Теперь давайте сравним это с мутационным тестированием. Я использую mutmut (полное раскрытие: я автор). Mutmut работает 16 секунд и находит 3 выживших мутантов. 16 секунд звучит много по сравнению с 3 секундами выше, но давайте помнить, что нам не нужно было писать тест выше, и я готов поспорить, что вы не сможете придумать этот тест И написать его правильно менее чем за 16 секунд. !

Чтобы запустить тестирование мутаций, нам нужно некоторое тестирование, поэтому я добавил тривиальный случай:

def test_a():
    assert mode([1, 2, 4, 4]) == 4

Небольшой экскурс в производительность

Также немного жаль, что mutmut порождает новый процесс pytest для каждого мутанта, и этот pytest имеет накладные расходы 0,7 с. В обычной работе это печально, в данном конкретном случае это выглядит очень-очень плохо. Я могу настроить mutmut следующим образом:

[mutmut]
runner = python -c "from src.mode import mode; assert mode([1, 2, 4, 4]) == 4"

…и сократить время полного прогона до менее секунды. Это раздражающее преимущество в производительности, основанное на тестировании (поскольку оно импортирует pytest только один раз), которое не имеет ничего общего с самой проблемной областью, а больше связано с деталями реализации и большими накладными расходами pytest.

Мутанты

Начнем с первого мутанта:

--- src/mode.py
+++ src/mode.py
@@ -3,7 +3,7 @@
     count = {}
     for x in l:
         if x not in count:
-            count[x] = 0
+            count[x] = 1
         count[x] += 1
         if not max or count[x] > count[max]:
             max = x

Этот мутант является ложноположительным, потому что нас на самом деле не волнует правильность подсчетов, просто чтобы они были правильными относительно друг друга, поэтому мы можем начать с любого случайного целого числа, и это будет работать нормально. . Но это также случай мутационного тестирования, говорящего нам, что этот код не является элегантным. Мы можем удалить всю эту строку и if над ней, если заменим count = {} на count = defaultdict(int) Это удалит мутант и упростит код. Выиграй, выиграй.

Переходим к следующему мутанту:

--- src/mode.py
+++ src/mode.py
@@ -4,7 +4,7 @@
     for x in l:
         if x not in count:
             count[x] = 0
-        count[x] += 1
+        count[x] += 2
         if not max or count[x] > count[max]:
             max = x
     return max

Это также ложное срабатывание, как и выше. Следующая мутация:

--- src/mode.py
+++ src/mode.py
@@ -5,7 +5,7 @@
         if x not in count:
             count[x] = 0
         count[x] += 1
-        if not max or count[x] > count[max]:
+        if not max or count[x] >= count[max]:
             max = x
     return max

Тоже ложноположительный. Нам все равно, вернем ли мы первый или последний объект в mode([1, 1, 1]).

Ну, это был бюст для mutmut! 😢 Оказывается, это показывает дыру в мутмуте. В нем отсутствует тип мутации, а именно замена None каким-то другим ложным значением, может быть, "":

--- src/mode.py
+++ src/mode.py
@@ -1,5 +1,5 @@
 def mode(l):
-    max = None
+    max = ""
     count = {}
     for x in l:
         if x not in count:

В коде даже есть примечание TODO, чтобы исправить это до того, как код стал общедоступным. Я забыл об этом, потому что его не было в трекере. Ой! Я исправил это, поэтому, если вы попробуете mutmut 1.5.0 сейчас, он обнаружит эту ошибку.

Преимущество мутационного тестирования перед тестированием на основе свойств

Преимущество мутационного тестирования по сравнению с тестированием на основе свойств заключается в том, что оно представляет собой перечисление конечной и довольно небольшой проблемной области. Тестирование на основе свойств — это исследование бесконечного проблемного пространства, а это означает, что вы на самом деле не знаете, закончили ли вы, и вам, скорее всего, все равно придется «белый ящик». Давайте возьмем крайний пример приведенного выше кода, где я исправил ошибку и ввел патологический случай (выделен жирным шрифтом):

def mode(l):
    if l == list(range(100)):
        return -1
    max = None
    count = {}
    for x in l:
        if x not in count:
            count[x] = 0
        count[x] += 1
        if max is None or count[x] > count[max]:
            max = x
    return max

Тест на основе свойств, который находил это раньше, теперь не находит. Мы можем сказать гипотезе, чтобы попробовать больше примеров:

@given(lists(integers(), min_size=1))
@settings(max_examples=50000)
def test_mode(l):
    x = mode(l)
    assert l.count(x) >= l.count(l[0])

Значение по умолчанию — 500, поэтому я увеличил его на два порядка. Теперь тесты на основе свойств выполняются 1 минута и не находят ошибку.

Теперь давайте посмотрим, что делает mutmut. Очень не доволен этим кодом! Он скажет вам, что может производить этих мутантов:

# mutant 2
--- src/mode.py
+++ src/mode.py
@@ -1,5 +1,5 @@
 def mode(l):
-    if l == list(range(100)):
+    if l == list(range(101)):
         return -1
 
     max = None
# mutant 3
--- src/mode.py
+++ src/mode.py
@@ -1,6 +1,6 @@
 def mode(l):
     if l == list(range(100)):
-        return -1
+        return +1
 
     max = None
     count = {}
# mutant 4
--- src/mode.py
+++ src/mode.py
@@ -1,6 +1,6 @@
 def mode(l):
     if l == list(range(100)):
-        return -1
+        return -2
 
     max = None
     count = {}

Теперь, очевидно, в этом случае простой отчет о покрытии также покажет вам, что что-то не так, но если мы изменим это, если:

if l == list(range(100)): return -1

то отчет о покрытии вам больше не поможет. Если только вы не включите охват ветвей, что вам следует сделать!

Надеюсь, вы попробуете мутационное тестирование своих библиотек. Для большинства проектов это так же просто, как:

> pip install mutmut
> mutmut run

Mutmut автоматически определяет, где находится ваш код и тесты, и просто запускается.