Las pruebas de mutación (o análisis de mutación o mutación de programa ) se utilizan para diseñar nuevas pruebas de software y evaluar la calidad de las pruebas de software existentes. Las pruebas de mutación implican modificar un programa en pequeñas formas. [1] Cada versión mutada se llama mutante y las pruebas detectan y rechazan mutantes haciendo que el comportamiento de la versión original sea diferente del mutante. Esto se llama matar al mutante. Las suites de pruebas se miden por el porcentaje de mutantes que matan. Se pueden diseñar nuevas pruebas para matar mutantes adicionales. Los mutantes se basan en operadores de mutación bien definidos que imitan errores de programación típicos (como usar el operador o el nombre de variable incorrectos) o fuerzan la creación de pruebas valiosas (como dividir cada expresión por cero). El propósito es ayudar al probador a desarrollar pruebas efectivas o localizar debilidades en los datos de prueba utilizados para el programa o en secciones del código a las que rara vez o nunca se accede durante la ejecución . Las pruebas de mutación son una forma de prueba de caja blanca . [2] [3]
La mayor parte de este artículo trata sobre la "mutación de programas", en la que se modifica el programa. Una definición más general del análisis de mutaciones es el uso de reglas bien definidas en estructuras sintácticas para realizar cambios sistemáticos en los artefactos de software. [4] El análisis de mutaciones se ha aplicado a otros problemas, pero normalmente se aplica a las pruebas. Por tanto, las pruebas de mutaciones se definen como el uso del análisis de mutaciones para diseñar nuevas pruebas de software o para evaluar pruebas de software existentes. [4] Por tanto, el análisis y las pruebas de mutaciones se pueden aplicar a modelos de diseño, especificaciones, bases de datos, pruebas, XML y otros tipos de artefactos de software, aunque la mutación de programas es la más común. [5]
Se pueden crear pruebas para verificar la corrección de la implementación de un sistema de software dado, pero la creación de pruebas aún plantea la pregunta de si las pruebas son correctas y cubren suficientemente los requisitos que han originado la implementación. [6] (Este problema tecnológico es en sí mismo un ejemplo de un problema filosófico más profundo llamado " Quis custodiet ipsos custodes? " ["¿Quién cuidará a los guardias?"].) La idea detrás de las pruebas de mutación es que si se introduce un mutante, esto normalmente causa un error en la funcionalidad del programa que las pruebas deberían encontrar. De esta manera, se prueban las pruebas. Si un mutante no es detectado por el conjunto de pruebas, esto generalmente indica que el conjunto de pruebas no puede localizar los fallos representados por el mutante, pero también puede indicar que la mutación no introduce fallos, es decir, la mutación es un cambio válido que no afecta la funcionalidad. Una forma (común) en que un mutante puede ser válido es que el código que se ha cambiado es "código muerto" que nunca se ejecuta.
Para que las pruebas de mutación funcionen a gran escala, se suele introducir una gran cantidad de mutantes, lo que lleva a la compilación y ejecución de una cantidad extremadamente grande de copias del programa. Este problema del costo de las pruebas de mutación había reducido su uso práctico como método de prueba de software. Sin embargo, el uso creciente de lenguajes de programación orientados a objetos y marcos de pruebas unitarias ha llevado a la creación de herramientas de prueba de mutación que prueban partes individuales de una aplicación.
Los objetivos de las pruebas de mutación son múltiples:
Las pruebas de mutación fueron propuestas originalmente por Richard Lipton cuando era estudiante en 1971, [8] y fueron desarrolladas y publicadas por primera vez por DeMillo, Lipton y Sayward. [1] La primera implementación de una herramienta de pruebas de mutación fue realizada por Timothy Budd como parte de su trabajo de doctorado (titulado Análisis de mutaciones ) en 1980 en la Universidad de Yale . [9]
Recientemente, con la disponibilidad de un poder computacional masivo, ha habido un resurgimiento del análisis de mutaciones dentro de la comunidad informática y se ha trabajado para definir métodos de aplicación de pruebas de mutaciones a lenguajes de programación orientados a objetos y lenguajes no procedimentales como XML , SMV y máquinas de estados finitos .
En 2004, una empresa llamada Certess Inc. (ahora parte de Synopsys ) extendió muchos de los principios al dominio de la verificación de hardware. Mientras que el análisis de mutaciones solo espera detectar una diferencia en el resultado producido, Certess extiende esto al verificar que un verificador en el banco de pruebas realmente detecte la diferencia. Esta extensión significa que se evalúan las tres etapas de verificación, a saber: activación, propagación y detección. A esto lo llamaron calificación funcional.
El fuzzing puede considerarse un caso especial de prueba de mutación. En el fuzzing, los mensajes o datos intercambiados dentro de las interfaces de comunicación (tanto dentro como entre instancias de software) se mutan para detectar fallas o diferencias en el procesamiento de los datos. Codenomicon [10] (2001) y Mu Dynamics (2005) desarrollaron los conceptos de fuzzing para una plataforma de prueba de mutación totalmente con estado, completa con monitores para ejercitar exhaustivamente las implementaciones de protocolos.
Las pruebas de mutación se basan en dos hipótesis. La primera es la hipótesis del programador competente . Esta hipótesis afirma que los programadores competentes escriben programas que están cerca de ser correctos. [1] La "cercanía" se pretende que se base en el comportamiento, no en la sintaxis. La segunda hipótesis se denomina efecto de acoplamiento . El efecto de acoplamiento afirma que los fallos simples pueden producirse en cascada o acoplarse para formar otros fallos emergentes. [11] [12]
Los mutantes de orden superior también revelan fallas sutiles e importantes, que refuerzan aún más el efecto de acoplamiento. [13] [14] [7] [15] [16] Los mutantes de orden superior se habilitan mediante la creación de mutantes con más de una mutación.
Las pruebas de mutación se realizan seleccionando un conjunto de operadores de mutación y luego aplicándolos al programa fuente de a uno por vez para cada fragmento aplicable del código fuente. El resultado de aplicar un operador de mutación al programa se denomina mutante . Si el conjunto de pruebas puede detectar el cambio (es decir, una de las pruebas falla), se dice que el mutante está eliminado .
Por ejemplo, considere el siguiente fragmento de código C++:
si ( a && b ) { c = 1 ; } de lo contrario { c = 0 ; }
El operador de mutación de condición reemplazaría &&
con ||
y produciría el siguiente mutante:
si ( a || b ) { c = 1 ; } de lo contrario { c = 0 ; }
Ahora bien, para que la prueba mate a este mutante, se deben cumplir las siguientes tres condiciones:
a = 1
y b = 0
haría esto.Estas condiciones se denominan colectivamente modelo RIP . [8]
Las pruebas de mutación débil (o cobertura de mutación débil ) requieren que solo se cumplan la primera y la segunda condición. Las pruebas de mutación fuerte requieren que se cumplan las tres condiciones. La mutación fuerte es más poderosa, ya que garantiza que el conjunto de pruebas realmente pueda detectar los problemas. La mutación débil está estrechamente relacionada con los métodos de cobertura de código . Requiere mucho menos poder de cómputo para garantizar que el conjunto de pruebas satisfaga las pruebas de mutación débil que las pruebas de mutación fuerte.
Sin embargo, hay casos en los que no es posible encontrar un caso de prueba que pueda matar a este mutante. El programa resultante es equivalente en comportamiento al original. Dichos mutantes se denominan mutantes equivalentes .
La detección de mutantes equivalentes es uno de los mayores obstáculos para el uso práctico de las pruebas de mutación. El esfuerzo necesario para comprobar si los mutantes son equivalentes o no puede ser muy alto incluso para programas pequeños. [17] Una revisión sistemática de la literatura de 2014 de una amplia gama de enfoques para superar el problema de mutantes equivalentes [18] identificó 17 técnicas relevantes (en 22 artículos) y tres categorías de técnicas: detección (DEM); sugerencia (SEM); y evitación de la generación de mutantes equivalentes (AEMG). El experimento indicó que la mutación de orden superior en general y la estrategia JudyDiffOp en particular proporcionan un enfoque prometedor para el problema de mutantes equivalentes.
Además de los mutantes equivalentes, existen mutantes subsumidos , que son mutantes que existen en la misma ubicación del código fuente que otro mutante y se dice que están "subsumidos" por el otro mutante. Los mutantes subsumidos no son visibles para una herramienta de prueba de mutaciones y no contribuyen a las métricas de cobertura. Por ejemplo, supongamos que tiene dos mutantes, A y B, que cambian una línea de código de la misma manera. Primero se prueba el mutante A y el resultado es que el código no funciona correctamente. Luego se prueba el mutante B y el resultado es el mismo que con el mutante A. En este caso, se considera que el mutante B está subsumido por el mutante A, ya que el resultado de la prueba del mutante B es el mismo que el resultado de la prueba del mutante A. Por lo tanto, no es necesario probar el mutante B, ya que el resultado será el mismo que el del mutante A.
Para realizar cambios sintácticos en un programa, un operador de mutación sirve como guía que sustituye partes del código fuente. Dado que las mutaciones dependen de estos operadores, los investigadores han creado una colección de operadores de mutación para adaptarse a diferentes lenguajes de programación, como Java. La eficacia de estos operadores de mutación desempeña un papel fundamental en las pruebas de mutación. [19]
Los investigadores han explorado muchos operadores de mutación. A continuación, se muestran algunos ejemplos de operadores de mutación para lenguajes imperativos:
goto fail;
[20]+
con *
, -
con/
>
con >=
, ==
y<=
Estos operadores de mutación también se denominan operadores de mutación tradicionales. También existen operadores de mutación para lenguajes orientados a objetos, [22] para construcciones concurrentes, [23] para objetos complejos como contenedores, [24] etc.
Los operadores para contenedores se denominan operadores de mutación a nivel de clase . Los operadores a nivel de clase modifican la estructura del programa añadiendo, eliminando o modificando las expresiones que se examinan. Se han establecido operadores específicos para cada categoría de cambios. [19] Por ejemplo, la herramienta muJava ofrece varios operadores de mutación a nivel de clase, como Access Modifier Change, Type Cast Operator Insertion y Type Cast Operator Deletion. También se han desarrollado operadores de mutación para realizar pruebas de vulnerabilidad de seguridad de los programas. [25]
Además de los operadores de nivel de clase , MuJava también incluye operadores de mutación de nivel de método , denominados operadores tradicionales. Estos operadores tradicionales están diseñados en función de características que se encuentran comúnmente en los lenguajes procedimentales. Llevan a cabo cambios en las declaraciones agregando, sustituyendo o eliminando operadores primitivos. Estos operadores se dividen en seis categorías: operadores aritméticos , operadores relacionales , operadores condicionales , operadores de desplazamiento , operadores lógicos y operadores de asignación . [19]
Hay tres tipos de pruebas de mutación:
La mutación de declaraciones es un proceso en el que se modifica intencionalmente un bloque de código, ya sea eliminando o copiando ciertas declaraciones. Además, permite reordenar las declaraciones dentro del bloque de código para generar varias secuencias. [26] Esta técnica es crucial en las pruebas de software, ya que ayuda a identificar posibles debilidades o errores en el código. Al realizar cambios deliberadamente en el código y observar cómo se comporta, los desarrolladores pueden descubrir errores o fallas ocultas que podrían pasar desapercibidas durante las pruebas habituales. [27] La mutación de declaraciones es como una herramienta de diagnóstico que proporciona información sobre la solidez y la resiliencia del código, lo que ayuda a los programadores a mejorar la calidad y la confiabilidad generales de su software.
Por ejemplo, en el fragmento de código a continuación, se elimina toda la sección 'else':
función checkCredentials ( nombre de usuario , contraseña ) { if ( nombre de usuario === "admin" && contraseña === "contraseña" ) { return true ; } }
La mutación de valores se produce cuando se ejecuta una modificación en los valores de los parámetros o de las constantes dentro del código. Esto suele implicar ajustar los valores sumando o restando 1, pero también puede implicar realizar cambios más sustanciales en los valores. Las modificaciones específicas que se realizan durante la mutación de valores incluyen dos escenarios principales:
En primer lugar, está la transformación de un valor pequeño a un valor más alto. Esto implica reemplazar un valor pequeño en el código por uno más grande. El propósito de este cambio es evaluar cómo responde el código cuando encuentra entradas más grandes. Ayuda a garantizar que el código pueda procesar de manera precisa y eficiente estos valores más grandes sin encontrar errores o problemas inesperados. [26]
Por el contrario, el segundo escenario implica cambiar un valor más alto por uno más pequeño. En este caso, reemplazamos un valor más alto dentro del código con un valor más pequeño. Esta prueba tiene como objetivo evaluar cómo el código maneja entradas más pequeñas. Asegurarse de que el código funciona correctamente con valores más pequeños es esencial para evitar problemas o errores imprevistos al tratar con dichos datos de entrada. [26]
Por ejemplo:
// Función del código original multiplicarPorDos ( valor ) { valor de retorno * 2 ; } // Mutación de valor: de valor pequeño a valor mayor function multiplicarPorDosMutation1 ( valor ) { valor de retorno * 10 ; } // Mutación de valor: función de valor mayor a valor menor multiplicarByTwoMutation2 ( valor ) { valor de retorno / 10 ; }
Las pruebas de mutación de decisiones se centran en la identificación de errores de diseño dentro del código, con especial énfasis en la detección de fallas o debilidades en la lógica de toma de decisiones del programa. Este método implica alterar deliberadamente los operadores aritméticos y lógicos para exponer posibles problemas. [26] Al manipular estos operadores, los desarrolladores pueden evaluar sistemáticamente cómo responde el código a diferentes escenarios de decisión. Este proceso ayuda a garantizar que las vías de toma de decisiones del programa sean sólidas y precisas, lo que evita errores costosos que podrían surgir de una lógica defectuosa. Las pruebas de mutación de decisiones sirven como una herramienta valiosa en el desarrollo de software, ya que permiten a los desarrolladores mejorar la confiabilidad y la eficacia de sus segmentos de código de toma de decisiones.
Por ejemplo:
// Función del código original isPositive ( número ) { return número > 0 ; } // Mutación de decisión: Cambiar la función del operador de comparación isPositiveMutation1 ( number ) { return number >= 0 ; } // Mutación de decisión: negar el resultado de la función isPositiveMutation2 ( number ) { return ! ( number > 0 ); }
{{cite book}}
: CS1 maint: bot: estado de URL original desconocido ( enlace )