Learning mutmut: una herramienta de prueba de mutaciones de Python

Las pruebas de mutación le permiten identificar errores que no están cubiertos por las pruebas convencionales.



¿Tienes pruebas para todas las ocasiones? ¿O quizás su repositorio de proyectos incluso contiene la ayuda "Aproximadamente el 100% de cobertura de prueba"? Pero, ¿es todo tan simple y alcanzable en la vida real?







Con las pruebas unitarias todo está más o menos claro: hay que escribirlas. A veces no funcionan como se esperaba: hay falsos positivos o pruebas con errores que devolverán sí y no sin ningún cambio de código. Los pequeños errores que puede encontrar en las pruebas unitarias son valiosos, pero a menudo el desarrollador los solucionará antes de que se cometan. Pero estamos realmente preocupados por esos errores que a menudo se pierden de vista. Y lo peor de todo, a menudo deciden hacerse un nombre justo cuando el producto cae en manos del usuario.



Es prueba de mutaciónle permite lidiar con errores tan insidiosos. Modifica el código fuente de una manera predeterminada (introduciendo errores especiales, los llamados "mutantes") y comprueba si estos mutantes sobreviven en otras pruebas. Cualquier mutante que sobreviviera a la prueba unitaria lleva a la conclusión de que las pruebas estándar no encontraron el código modificado correspondiente que contiene el error.



En Python, mutmut es la herramienta principal para las pruebas de mutación .



Imagina que necesitamos escribir un código que calcule el ángulo entre las manecillas de las horas y los minutos en un reloj analógico:



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))


Escribamos una prueba unitaria básica:



import angle

def test_twelve():
    assert angle.between(12, 00) == 0


No hay si en el código . Veamos en qué medida dicha prueba unitaria cubre todas las situaciones posibles:



$ 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 ===============================


¡Excelente! Como una cobertura del 100%. Pero, ¿qué sucede cuando hacemos pruebas de mutación?







¡Oh no! De los 21, sobrevivieron hasta 16 mutantes. ¿Cómo es eso?



Para cada prueba de mutación, es necesario modificar una parte del código fuente que simula un error potencial. Un ejemplo de tal modificación es cambiar el operador de comparación ">" a "> =". Si no hay una prueba unitaria para esta condición de límite, este error mutante sobrevivirá: este es un error potencial que ninguna de las pruebas habituales detectará.



Bueno. Todo claro. Necesitamos escribir mejores pruebas unitarias. Luego, usando el comando de resultados, veamos qué cambios específicos se realizaron:



$ 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


Este es un ejemplo típico de cómo funciona mumut: analiza el código fuente y reemplaza algunos operadores por otros: por ejemplo, suma por resta o, como en este caso, multiplicación por división. Las pruebas unitarias, en general, deben detectar errores al cambiar declaraciones; de lo contrario, no prueban eficazmente el comportamiento del programa. Esta es la lógica a la que se adhiere mutmut cuando realiza ciertos cambios.



Podemos usar el comando mutmut apply en el mutante superviviente. Vaya, resulta que no comprobamos si el parámetro de hora se utilizó correctamente. Arreglemos esto:



$ 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


Anteriormente, solo verificamos 12. ¿Agregar un cheque para el valor 3 salvaría el día?







Esta nueva prueba ha logrado matar dos mutantes: es mejor que antes, pero se necesita hacer más trabajo. No escribiré una solución para cada uno de los 14 casos restantes en este momento, porque la idea ya es clara (¿puedes matar a todos los mutantes tú mismo?)



Además de medir la cobertura, las pruebas de mutación también te permiten evaluar qué tan completas son tus pruebas. De esta forma puedes mejorar las pruebas: cualquiera de los mutantes supervivientes es un error que puede haber cometido el desarrollador, así como un posible defecto en tu producto. Entonces, ¡deseo que mates más mutantes!






Publicidad



VDSina ofrece servidores virtuales para Linux y Windows : elija uno de los sistemas operativos preinstalados o instálelo desde su imagen.






All Articles