unit testing - tipos - ¿Las pruebas invariantes pueden reemplazar las pruebas unitarias?
tipos de pruebas unitarias (4)
Dudoso
Solo escuché (no usé) este tipo de pruebas, pero veo dos posibles problemas. Me encantaría tener comentarios sobre cada uno.
Resultados engañosos
He oído hablar de pruebas como:
-
reverse(reverse(list))
debería ser igual -
unzip(zip(data))
debería ser igual a losdata
Sería genial saber que esto es válido para una amplia gama de entradas. Pero ambas pruebas pasarían si las funciones simplemente devuelven su entrada.
Me parece que querrá verificar que, por ejemplo, reverse([1 2 3])
igual a [3 2 1]
para demostrar el comportamiento correcto en al menos un caso, luego agregue algunas pruebas con datos aleatorios.
Complejidad de la prueba
Una prueba invariante que describa completamente la relación entre la entrada y la salida podría ser más compleja que la función misma. Si es complejo, podría tener errores, pero no tienes pruebas para tus pruebas.
Una buena prueba de unidad, por el contrario, es demasiado simple de confundir o malinterpretar como lector. Solo un error tipográfico podría crear un error en "esperar reverse([1 2 3])
a igual [3 2 1]
".
Como programador, he comprado de todo corazón la filosofía de TDD y me esfuerzo por hacer extensas pruebas de unidad para cualquier código no trivial que escribo. A veces, este camino puede ser doloroso (cambios de comportamiento que provocan cambios en cascada de múltiples unidades, se necesitan grandes cantidades de andamios), pero en general me niego a programar sin pruebas que puedo ejecutar después de cada cambio, y mi código es mucho menos complicado como resultado.
Recientemente, he estado jugando con Haskell, y es la biblioteca de prueba residente, QuickCheck. De una manera distintivamente diferente de TDD, QuickCheck tiene un énfasis en probar invariantes del código, es decir, ciertas propiedades que se mantienen sobre todos (o subconjuntos sustantivos) de las entradas. Un ejemplo rápido: un algoritmo de clasificación estable debería dar la misma respuesta si lo ejecutamos dos veces, debería tener una producción creciente, debería ser una permutación de la entrada, etc. Entonces, QuickCheck genera una variedad de datos aleatorios para probar estas invariantes.
Me parece, al menos para funciones puras (es decir, funciones sin efectos secundarios, y si lo haces burlonamente puedes convertir funciones sucias en puras), esa prueba invariante podría suplantar la prueba unitaria como un superconjunto estricto de esas capacidades. . Cada prueba unitaria consiste en una entrada y una salida (en los lenguajes de programación imperativos, la "salida" no es solo el retorno de la función sino también cualquier estado modificado, pero esto puede ser encapsulado). Uno podría concebiblemente crear un generador de entrada aleatorio que sea lo suficientemente bueno para cubrir todas las entradas de prueba de la unidad que usted habría creado manualmente (y algo más, porque generaría casos en los que no habría pensado); Si encuentra un error en su programa debido a alguna condición de contorno, usted mejora su generador de entrada aleatoria para que genere ese caso también.
El desafío, entonces, es si es posible o no formular invariantes útiles para cada problema. Yo diría que sí: es mucho más simple una vez que tienes una respuesta para ver si es correcta de lo que es calcular la respuesta en primer lugar. Pensar en invariantes también ayuda a aclarar la especificación de un algoritmo complejo mucho mejor que los casos de prueba ad hoc, que fomentan una especie de pensamiento caso por caso del problema. Puede utilizar una versión anterior de su programa como una implementación modelo, o una versión de un programa en otro idioma. Etc. Eventualmente, podría cubrir todos sus casos de prueba anteriores sin tener que codificar explícitamente una entrada o una salida.
¿Me he vuelto loco o tengo algo?
Lo que escribiste en tu publicación original me recordó este problema, que es una pregunta abierta sobre qué invariante de bucle es para demostrar que el bucle es correcto ...
de todos modos, no estoy seguro de cuánto has leído en las especificaciones formales, pero te diriges hacia esa línea de pensamiento. El libro de david gries es uno de los clásicos sobre el tema, todavía no domino el concepto lo suficientemente bien como para usarlo rápidamente en mi programación diaria. la respuesta habitual a las especificaciones formales es, es difícil y complicada, y solo vale la pena el esfuerzo si está trabajando en sistemas críticos de seguridad. pero creo que hay técnicas de sobre de respaldo similares a lo que expone Quickcheck que puede usarse.
Lo que has mencionado es un muy buen punto, cuando solo se aplica a la programación funcional. Usted indicó un medio para lograr todo esto con un código imperativo, pero también mencionó por qué no lo hizo, no es particularmente fácil.
Creo que esa es la razón por la que no reemplazará las pruebas unitarias: no se ajusta al código imperativo con la misma facilidad.
Un año después, ahora creo que tengo una respuesta a esta pregunta: ¡No! En particular, las pruebas unitarias siempre serán necesarias y útiles para las pruebas de regresión, en las que se adjunta una prueba a un informe de error y permanece en la base de código para evitar que vuelva a aparecer.
Sin embargo, sospecho que cualquier prueba de unidad se puede reemplazar con una prueba cuyas entradas se generan de forma aleatoria. Incluso en el caso del código imperativo, la "entrada" es el orden de las declaraciones imperativas que necesita realizar. Por supuesto, si vale la pena crear el generador de datos aleatorios y si puede hacer que el generador de datos aleatorios tenga la distribución correcta es otra cuestión. La prueba unitaria es simplemente un caso degenerado donde el generador aleatorio siempre da el mismo resultado.