¿Cómo se escriben las pruebas unitarias para el código con resultados difíciles de predecir?

124

Con frecuencia trabajo con programas muy numéricos / matemáticos, donde el resultado exacto de una función es difícil de predecir de antemano.

Al tratar de aplicar TDD con este tipo de código, a menudo encuentro que escribir el código bajo prueba es significativamente más fácil que escribir pruebas unitarias para ese código, porque la única forma que sé para encontrar el resultado esperado es aplicar el algoritmo mismo (ya sea en mi cabeza, en papel o por la computadora). Esto se siente mal, porque estoy usando efectivamente el código bajo prueba para verificar mis pruebas unitarias, en lugar de al revés.

¿Existen técnicas conocidas para escribir pruebas unitarias y aplicar TDD cuando el resultado del código bajo prueba es difícil de predecir?

Un ejemplo (real) de código con resultados difíciles de predecir:

Una función weightedTasksOnTimeque, dada la cantidad de trabajo realizado por día workPerDayen el rango (0, 24], la hora actual initialTime> 0 y una lista de tareas taskArray; cada una con un tiempo para completar la propiedad time> 0, fecha de vencimiento duey valor de importancia importance; devuelve un valor normalizado en el rango [0, 1] que representa la importancia de las tareas que se pueden completar antes de su duefecha si cada tarea se completa en el orden dado por taskArray, comenzando en initialTime.

El algoritmo para implementar esta función es relativamente sencillo: iterar sobre las tareas en taskArray. Para cada tarea, agregue timea initialTime. Si la nueva hora < due, agregue importancea un acumulador. El tiempo se ajusta mediante trabajo inverso por día. Antes de devolver el acumulador, divídalo por la suma de las tareas importantes para normalizar.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Creo que el problema anterior se puede simplificar, manteniendo su núcleo, eliminando workPerDayy el requisito de normalización, para dar:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

Esta pregunta aborda situaciones en las que el código bajo prueba no es una reimplementación de un algoritmo existente. Si el código es una reimplementación, intrínsecamente tiene resultados fáciles de predecir, porque las implementaciones confiables existentes del algoritmo actúan como un oráculo de prueba natural.

PaintingInAir
fuente
44
¿Puede proporcionar un ejemplo simple de una función cuyo resultado es difícil de predecir?
Robert Harvey
6262
FWIW no estás probando el algoritmo. Presumiblemente eso es correcto. Estás probando la implementación. Hacer ejercicio a mano suele estar bien como una construcción paralela.
Kristian H
77
Hay situaciones en las que un algoritmo no puede ser razonablemente probado en la unidad, por ejemplo, si su tiempo de ejecución es de varios días / meses. Esto puede suceder al resolver problemas de NP. En estos casos, puede ser más factible proporcionar una prueba formal de que el código es correcto.
Hulk
12
Algo que he visto en un código numérico muy complicado es tratar las pruebas unitarias solo como pruebas de regresión. Escriba la función, ejecútela para varios valores interesantes, valide los resultados manualmente, luego escriba la prueba unitaria para capturar las regresiones del resultado esperado. Codificación de horror? Curioso lo que otros piensan.
Chuu

Respuestas:

251

Hay dos cosas que puede probar en un código difícil de probar. Primero, los casos degenerados. Qué sucede si no tiene elementos en su matriz de tareas, o solo uno, o dos, pero uno ha pasado la fecha de vencimiento, etc. Cualquier cosa que sea más simple que su problema real, pero aún así razonable para calcular manualmente.

El segundo son los controles de cordura. Estas son las comprobaciones que realiza cuando no sabe si una respuesta es correcta , pero definitivamente sabrá si está equivocada . Estas son cosas como el tiempo debe avanzar, los valores deben estar en un rango razonable, los porcentajes deben sumar 100, etc.

Sí, esto no es tan bueno como una prueba completa, pero te sorprendería la frecuencia con la que te equivocas en los controles de cordura y los casos degenerados, lo que revela un problema en tu algoritmo completo.

Karl Bielefeldt
fuente
54
Creo que este es un muy buen consejo. Comience escribiendo este tipo de pruebas unitarias. A medida que desarrolle el software, si encuentra errores o respuestas incorrectas, agréguelos como pruebas unitarias. Haga lo mismo, hasta cierto punto, cuando encuentre respuestas definitivamente correctas. Constrúyalos con el tiempo, y usted (eventualmente) tendrá un conjunto muy completo de pruebas unitarias a pesar de comenzar sin saber lo que iban a ser ...
Algy Taylor
21
Otra cosa que podría ser útil en algunos casos (aunque quizás no este) es escribir una función inversa y probar que, cuando está encadenado, su entrada y salida son las mismas.
Cyberspark el
77
el control de cordura suele ser un buen objetivo para las pruebas basadas en propiedades con algo como QuickCheck
jk.
10
La otra categoría de pruebas que recomendaría son algunas para verificar cambios involuntarios en la salida. Puede 'engañar' a estos utilizando el código mismo para generar el resultado esperado, ya que la intención de estos es ayudar a los mantenedores al señalar que algo destinado como un cambio neutral de salida afectó involuntariamente el comportamiento algorítmico.
Dan Neely
55
@iFlo No estoy seguro si estaba bromeando, pero el inverso inverso ya existe. Sin embargo
vale la
80

Solía ​​escribir pruebas para software científico con resultados difíciles de predecir. Hicimos mucho uso de las relaciones metamórficas. Esencialmente, hay cosas que sabe acerca de cómo debe comportarse su software, incluso si no conoce resultados numéricos exactos.

Un posible ejemplo para su caso: si disminuye la cantidad de trabajo que puede hacer cada día, la cantidad total de trabajo que puede hacer será, en el mejor de los casos, igual, pero probablemente disminuirá. Por lo tanto, ejecute la función para varios valores de workPerDayy asegúrese de que la relación se mantenga.

James Elderfield
fuente
32
Las relaciones metamórficas son un ejemplo específico de pruebas basadas en propiedades , que en general es una herramienta útil para situaciones como estas
Dannnno
38

Las otras respuestas tienen buenas ideas para desarrollar pruebas para el caso de borde o error. Para los demás, el uso del algoritmo en sí no es ideal (obviamente) pero sigue siendo útil.

Detectará si el algoritmo (o los datos de los que depende) ha cambiado

Si el cambio es un accidente, podría revertir una confirmación. Si el cambio fue deliberado, debe volver a visitar la prueba de la unidad.

user949300
fuente
66
Y para el registro, este tipo de pruebas a menudo se denominan "pruebas de regresión" según su propósito, y son básicamente una red de seguridad para cualquier modificación / refactorización.
Pac0
21

De la misma manera que escribe pruebas unitarias para cualquier otro tipo de código:

  1. Encuentre algunos casos de prueba representativos y pruébelos.
  2. Encuentra casos extremos y pruébalos.
  3. Encuentra condiciones de error y pruébalas.

A menos que su código implique algún elemento aleatorio o no sea determinista (es decir, no producirá la misma salida dada la misma entrada), es comprobable por unidad.

Evite los efectos secundarios o las funciones afectadas por fuerzas externas. Las funciones puras son más fáciles de probar.

Robert Harvey
fuente
2
Para algoritmos no deterministas, puede guardar la semilla de RNG o simularlo usando una secuencia fija o una serie determinista de baja discrepancia, por ejemplo, la secuencia de Halton
wondra
14
@PaintingInAir Si es imposible verificar la salida del algoritmo, ¿puede el algoritmo ser incorrecto?
WolfgangGroiss
55
Unless your code involves some random elementEl truco aquí es hacer que su generador de números aleatorios sea una dependencia inyectada, para que pueda reemplazarlo por un generador de números que le dé el resultado exacto que desea. Esto le permite volver a probar con precisión, contando también los números generados como parámetros de entrada. not deterministic (i.e. it won't produce the same output given the same input)Dado que una prueba unitaria debe comenzar desde una situación controlada , solo puede ser no determinista si tiene un elemento aleatorio, que luego puede inyectar. No puedo pensar en otras posibilidades aquí.
Flater
3
@PaintingInAir: O o. Mi comentario se aplica tanto a la ejecución rápida como a la escritura de prueba rápida. Si le lleva tres días calcular un solo ejemplo a mano (supongamos que usa el método más rápido disponible que no usa el código), entonces tres días es lo que tomará. Si, en cambio, basó el resultado esperado de la prueba en el código real, entonces la prueba se compromete. Eso es como hacerlo if(x == x), es una comparación sin sentido. Necesita sus dos resultados ( real : proviene del código; esperado : proviene de su conocimiento externo) para ser independientes el uno del otro.
Flater
2
Todavía es comprobable por unidad, incluso si no es determinista, siempre que cumpla con las especificaciones y que se pueda medir el cumplimiento (por ejemplo, distribución y distribución aleatoria). Puede que se requieran muchas muestras para eliminar el riesgo de anomalía.
mckenzm
17

Actualización debido a comentarios publicados

La respuesta original se eliminó por razones de brevedad: puede encontrarla en el historial de edición.

PaintingInAir Por contexto: como emprendedor y académico, la mayoría de los algoritmos que diseño no son solicitados por nadie más que por mí. El ejemplo dado en la pregunta es parte de un optimizador sin derivados para maximizar la calidad de un ordenamiento de tareas. En términos de cómo describí internamente la necesidad de la función de ejemplo: "Necesito una función objetiva para maximizar la importancia de las tareas que se completan a tiempo". Sin embargo, todavía parece haber una gran brecha entre esta solicitud y la implementación de pruebas unitarias.

Primero, un TL; DR para evitar una respuesta larga:

Piénselo de esta manera:
un cliente ingresa a McDonald's y pide una hamburguesa con lechuga, tomate y jabón de manos como ingredientes. Esta orden se le da al cocinero, quien hace la hamburguesa exactamente como se solicitó. ¡El cliente recibe esta hamburguesa, se la come y luego se queja al cocinero de que no es una hamburguesa sabrosa!

Esto no es culpa del cocinero: solo está haciendo lo que el cliente le pidió explícitamente. No es tarea del cocinero verificar si el pedido solicitado es realmente sabroso . El cocinero simplemente crea lo que el cliente ordena. Es responsabilidad del cliente pedir algo que le parezca sabroso .

Del mismo modo, el trabajo del desarrollador no es cuestionar la exactitud del algoritmo. Su único trabajo es implementar el algoritmo según lo solicitado.
Las pruebas unitarias son una herramienta para desarrolladores. Confirma que la hamburguesa coincide con el pedido (antes de salir de la cocina). No intenta (y no debe) confirmar que la hamburguesa solicitada es realmente sabrosa.

Incluso si usted es tanto el cliente como el cocinero, todavía existe una distinción significativa entre:

  • No preparé esta comida correctamente, no estaba sabrosa (= error de cocción). Un filete quemado nunca tendrá buen sabor, incluso si te gusta el filete.
  • He preparado la comida correctamente, pero no me gusta (= error del cliente). Si no te gusta el bistec, nunca te gustará comerlo, incluso si lo cocinaste a la perfección.

El problema principal aquí es que no está haciendo una separación entre el cliente y el desarrollador (y el analista, aunque ese rol también puede ser representado por un desarrollador).

Debe distinguir entre probar el código y probar los requisitos comerciales.

Por ejemplo, el cliente quiere que funcione así [esto] . Sin embargo, el desarrollador malinterpreta y escribe un código que hace [eso] .

Por lo tanto, el desarrollador escribirá pruebas unitarias que prueben si [eso] funciona como se esperaba. Si desarrolló la aplicación correctamente, sus pruebas unitarias pasarán aunque la aplicación no haga [esto] , lo que el cliente esperaba.

Si desea probar las expectativas del cliente (los requisitos comerciales), eso debe hacerse en un paso separado (y posterior).

Un flujo de trabajo de desarrollo simple para mostrarle cuándo deben ejecutarse estas pruebas:

  • El cliente explica el problema que quiere resolver.
  • El analista (o desarrollador) escribe esto en un análisis.
  • El desarrollador escribe código que hace lo que describe el análisis.
  • El desarrollador prueba su código (pruebas unitarias) para ver si siguió el análisis correctamente
  • Si las pruebas unitarias fallan, el desarrollador vuelve al desarrollo. Esto se repite indefinidamente, hasta que la unidad prueba todas las pasadas.
  • Ahora que tiene una base de código probada (confirmada y aprobada), el desarrollador crea la aplicación.
  • La aplicación se entrega al cliente.
  • El cliente ahora prueba si la aplicación que se le da realmente resuelve el problema que buscaba resolver (pruebas de control de calidad) .

Quizás se pregunte cuál es el punto de hacer dos pruebas separadas cuando el cliente y el desarrollador son uno y lo mismo. Como no hay una "transferencia" del desarrollador al cliente, las pruebas se ejecutan una tras otra, pero aún son pasos separados.

  • Las pruebas unitarias son una herramienta especializada que lo ayuda a verificar si su etapa de desarrollo ha finalizado.
  • Las pruebas de control de calidad se realizan mediante el uso de la aplicación .

Si desea probar si su algoritmo es correcto, eso no es parte del trabajo del desarrollador . Esa es la preocupación del cliente, y el cliente lo probará utilizando la aplicación.

Como emprendedor y académico, es posible que te falte una distinción importante aquí, que destaca las diferentes responsabilidades.

  • Si la aplicación no cumple con lo que el cliente había pedido inicialmente, los cambios posteriores al código generalmente se realizan de forma gratuita ; ya que es un error del desarrollador. El desarrollador cometió un error y debe pagar el costo de rectificarlo.
  • Si la aplicación hace lo que el cliente había pedido inicialmente, pero ahora el cliente ha cambiado de opinión (por ejemplo, ha decidido utilizar un algoritmo diferente y mejor), los cambios en la base del código se cobran al cliente , ya que no es culpa del desarrollador de que el cliente haya pedido algo diferente de lo que ahora quiere. Es responsabilidad del cliente (costo) cambiar de opinión y, por lo tanto, hacer que los desarrolladores dediquen más esfuerzos para desarrollar algo que no se acordó previamente.
Flater
fuente
Me encantaría ver más detalles sobre la situación "Si se le ocurrió el algoritmo usted mismo", ya que creo que esta es la situación con mayor probabilidad de presentar problemas. Especialmente en situaciones donde no se proporcionan ejemplos "si A entonces B, sino C". (ps no soy el votante negativo)
PaintingInAir
@PaintingInAir: Pero realmente no puedo dar más detalles sobre esto, ya que depende de su situación. Si decidió crear este algoritmo, obviamente lo hizo para proporcionar una característica particular. ¿Quién te pidió que lo hicieras? ¿Cómo describieron su solicitud? ¿Te dijeron lo que necesitaban que sucediera en ciertos escenarios? (esta información es a lo que me refiero como "el análisis" en mi respuesta) Cualquier explicación que haya recibido (que lo llevó a crear el algoritmo) puede usarse para probar si el algoritmo funciona según lo solicitado. En resumen, se puede utilizar cualquier cosa menos el algoritmo de código / creación propia .
Flater
2
@PaintingInAir: Es peligroso acoplar estrechamente al cliente, analista y desarrollador; ya que es propenso a omitir pasos esenciales como definir el comienzo del problema . Creo que eso es lo que estás haciendo aquí. Parece que desea probar la corrección del algoritmo, en lugar de si se implementó correctamente. Pero no es así como lo haces. La prueba de la implementación se puede hacer usando pruebas unitarias. Probar el algoritmo en sí mismo es cuestión de utilizar su aplicación (probada) y verificar sus resultados: esta prueba real está fuera del alcance de su base de código (como debería ser ).
Flater
44
Esta respuesta ya es enorme. Recomiendo encarecidamente tratar de encontrar una forma de reformular el contenido original para que pueda integrarlo en la nueva respuesta si no desea tirarlo.
jpmc26
77
Además, no estoy de acuerdo con tu premisa. Las pruebas pueden y deben revelar absolutamente cuándo el código genera una salida incorrecta de acuerdo con las especificaciones. Es válido para las pruebas para validar las salidas de algunos casos de prueba conocidos. Además, el cocinero debe saber mejor que aceptar el "jabón de manos" como un ingrediente válido para hamburguesas, y el empleador casi seguramente ha educado al cocinero sobre qué ingredientes están disponibles.
jpmc26
9

Prueba de propiedad

A veces, las funciones matemáticas se cumplen mejor con "Pruebas de propiedad" que con las pruebas unitarias tradicionales basadas en ejemplos. Por ejemplo, imagine que está escribiendo pruebas unitarias para algo como una función de "multiplicación" de enteros. Si bien la función en sí misma puede parecer muy simple, si es la única forma de multiplicarse, ¿cómo se prueba a fondo sin la lógica en la función misma? Podría usar tablas gigantes con entradas / salidas esperadas, pero esto es limitado y propenso a errores.

En estos casos, puede probar las propiedades conocidas de la función, en lugar de buscar resultados esperados específicos. Para la multiplicación, puede saber que multiplicar un número negativo y un número positivo debería dar como resultado un número negativo, y que multiplicar dos números negativos debería dar como resultado un número positivo, etc. Usando valores aleatorios y luego verificando que estas propiedades se conservan para todos Los valores de prueba son una buena manera de probar tales funciones. Generalmente necesita probar más de una propiedad, pero a menudo puede identificar un conjunto finito de propiedades que juntas validan el comportamiento correcto de una función sin conocer necesariamente el resultado esperado para cada caso.

Una de las mejores presentaciones de Property Testing que he visto es esta en F #. Esperemos que la sintaxis no sea un obstáculo para comprender la explicación de la técnica.

Aaron M. Eshbach
fuente
1
Sugeriría quizás agregar algo un poco más específico en su ejemplo re multiplicación, como generar cuartetos aleatorios (a, b, c) y confirmar que (ab) (cd) produce (ac-ad) - (bc-bd). Una operación de multiplicación podría estar bastante rota y mantener la regla (negativo por rendimiento negativo positivo), pero la regla distributiva predice resultados específicos.
supercat
4

Es tentador escribir el código y luego ver si el resultado "se ve bien", pero, como intuye correctamente, no es una buena idea.

Cuando el algoritmo es difícil, puede hacer varias cosas para facilitar el cálculo manual del resultado.

  1. Usa Excel. Configure una hoja de cálculo que haga algunos o todos los cálculos por usted. Mantenlo lo suficientemente simple para que puedas ver los pasos.

  2. Divida su método en métodos comprobables más pequeños, cada uno con sus propias pruebas. Cuando esté seguro de que las piezas más pequeñas funcionan, úselas para trabajar manualmente en el siguiente paso.

  3. Use propiedades agregadas para verificar la cordura. Por ejemplo, supongamos que tiene una calculadora de probabilidad; Es posible que no sepa cuáles deberían ser los resultados individuales, pero sabe que todos tienen que sumar 100%.

  4. Fuerza bruta. Escriba un programa que genere todos los resultados posibles y verifique que ninguno sea mejor de lo que genera su algoritmo.

Ewan
fuente
Para 3., permita algunos errores de redondeo aquí. Es posible que su monto total sea 100,000001% o cifras similares cercanas pero no exactas.
Flater
2
No estoy muy seguro acerca de 4. Si puede generar el resultado óptimo para todas las combinaciones de entrada posibles (que luego utiliza para la confirmación de la prueba), entonces es inherentemente capaz de calcular el resultado óptimo y, por lo tanto, no ' No necesita esta segunda porción de código que está intentando probar. En ese punto, sería mejor usar su generador de resultados óptimo existente, ya que ya está demostrado que funciona. (y si aún no se ha demostrado que funcione, entonces no puede confiar en su resultado para verificar sus pruebas para comenzar).
Flater
66
@flater generalmente tiene otros requisitos, así como la corrección que la fuerza bruta no cumple. Por ejemplo, el rendimiento.
Ewan
1
@flater Odiaría usar tu tipo, ruta más corta, motor de ajedrez, etc., si crees eso. Pero apostaría totalmente en su error de redondeo permitido casino todo el día
Ewan
3
@flater, ¿renuncias cuando llegas a un juego final de peón rey? el hecho de que todo el juego no pueda ser forzado en bruto no significa una posición individual que no pueda. El hecho de que fuerza bruta el camino más corto correcto a una red no significa que conozca el camino más corto en todas las redes
Ewan
2

TL; DR

Diríjase a la sección de "pruebas comparativas" para obtener consejos que no figuran en otras respuestas.


Principios

Comience probando los casos que deben ser rechazados por el algoritmo (cero o negativo workPerDay, por ejemplo) y los casos que son triviales (por ejemplo, tasksmatriz vacía ).

Después de eso, primero desea probar los casos más simples. Para la tasksentrada, necesitamos probar diferentes longitudes; debería ser suficiente para probar los elementos 0, 1 y 2 (2 pertenece a la categoría "muchos" para esta prueba).

Si puede encontrar entradas que puedan calcularse mentalmente, es un buen comienzo. Una técnica que a veces uso es comenzar desde un resultado deseado y retroceder (en la especificación) a las entradas que deberían producir ese resultado.

Prueba comparativa

A veces, la relación de la salida con la entrada no es obvia, pero tiene una relación predecible entre diferentes salidas cuando se cambia una entrada. Si he entendido el ejemplo correctamente, entonces agregar una tarea (sin cambiar otras entradas) nunca aumentará la proporción del trabajo realizado a tiempo, por lo que podemos crear una prueba que llame a la función dos veces, una con y otra sin la tarea adicional - y afirma la desigualdad entre los dos resultados.

Fallbacks

A veces he tenido que recurrir a un comentario largo que muestra un resultado calculado a mano en pasos correspondientes a la especificación (dicho comentario suele ser más largo que el caso de prueba). El peor de los casos es cuando tiene que mantener la compatibilidad con una implementación anterior en un idioma diferente o para un entorno diferente. A veces solo tiene que etiquetar los datos de prueba con algo así /* derived from v2.6 implementation on ARM system */. Eso no es muy satisfactorio, pero puede ser aceptable como prueba de fidelidad al portar, o como una muleta a corto plazo.

Recordatorios

El atributo más importante de una prueba es su legibilidad: si las entradas y salidas son opacas para el lector, entonces la prueba tiene un valor muy bajo, pero si se ayuda al lector a comprender las relaciones entre ellos, entonces la prueba tiene dos propósitos.

No olvide utilizar un "aproximadamente igual" apropiado para obtener resultados inexactos (por ejemplo, coma flotante).

Evite las pruebas en exceso: solo agregue una prueba si cubre algo (como un valor límite) que otras pruebas no alcanzan.

Toby Speight
fuente
2

No hay nada muy especial en este tipo de función difícil de probar. Lo mismo se aplica para el código que usa interfaces externas (por ejemplo, una API REST de una aplicación de terceros que no está bajo su control y que ciertamente no puede ser probada por su conjunto de pruebas; o usando una biblioteca de terceros donde no está seguro de la formato de byte exacto de los valores de retorno).

Es un enfoque bastante válido simplemente ejecutar su algoritmo para una entrada sensata, ver qué hace, asegurarse de que el resultado sea correcto y encapsular la entrada y el resultado como un caso de prueba. Puede hacer esto por algunos casos y así obtener varias muestras. Intente hacer que los parámetros de entrada sean lo más diferentes posible. En el caso de una llamada API externa, haría algunas llamadas contra el sistema real, las rastrearía con alguna herramienta y luego las imitaría en las pruebas unitarias para ver cómo reacciona su programa, que es lo mismo que elegir algunas ejecuta su código de planificación de tareas, verificándolos a mano y luego codificando el resultado en sus pruebas.

Luego, obviamente, traiga casos extremos como (en su ejemplo) una lista vacía de tareas; ese tipo de cosas.

Su conjunto de pruebas quizás no sea tan bueno como para un método en el que puede predecir fácilmente los resultados; pero sigue siendo 100% mejor que ninguna suite de pruebas (o simplemente una prueba de humo).

Sin embargo, si su problema es que le resulta difícil decidir si un resultado es correcto, entonces ese es un problema completamente diferente. Por ejemplo, supongamos que tiene un método que detecta si un número arbitrariamente grande es primo. Difícilmente puede arrojarle un número aleatorio y luego simplemente "mirar" si el resultado es correcto (suponiendo que no pueda decidir la primacía en su cabeza o en una hoja de papel). En este caso, de hecho, es poco lo que puede hacer: necesitaría obtener resultados conocidos (es decir, algunos números primos grandes) o implementar la funcionalidad con un algoritmo diferente (tal vez incluso un equipo diferente: la NASA parece ser aficionada a eso) y espero que si cualquiera de las implementaciones tiene errores, al menos el error no conduce a los mismos resultados incorrectos.

Si este es un caso normal para usted, entonces debe tener una buena conversación con sus ingenieros de requisitos. Si no pueden formular sus requisitos de una manera que sea fácil (o posible) verificar por usted, entonces, ¿cuándo sabe si ha terminado?

AnoE
fuente
2

Otras respuestas son buenas, así que intentaré acertar en algunos puntos que se han perdido colectivamente hasta ahora.

He escrito (y probado exhaustivamente) software para hacer procesamiento de imágenes usando Synthetic Aperture Radar (SAR). Es de naturaleza científica / numérica (hay mucha geometría, física y matemáticas involucradas).

Un par de consejos (para pruebas científicas / numéricas generales):

1) Usa inversas. ¿Cuál es la fftde [1,2,3,4,5]? Ni idea. ¿Qué es ifft(fft([1,2,3,4,5]))? Debería estar [1,2,3,4,5](o cerca de él, pueden aparecer errores de coma flotante). Lo mismo ocurre con el caso 2D.

2) Use afirmaciones conocidas. Si escribe una función determinante, puede ser difícil decir cuál es el determinante de una matriz aleatoria de 100x100. Pero sí sabe que el determinante de la matriz de identidad es 1, incluso si es 100x100. También sabe que la función debe devolver 0 en una matriz no invertible (como un 100x100 lleno de todos los 0).

3) Use afirmaciones aproximadas en lugar de afirmaciones exactas . Escribí un código para dicho procesamiento SAR que registraría dos imágenes al generar puntos de enlace que crean un mapeo entre las imágenes y luego hacer una deformación entre ellas para que coincidan. Podría registrarse en un nivel de subpíxel. A priori, es difícil decir algo sobre cómo podría ser el registro de dos imágenes. ¿Cómo puedes probarlo? Cosas como:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

Como solo puede registrarse en partes superpuestas, la imagen registrada debe ser más pequeña o igual a su imagen más pequeña, y también:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

dado que una imagen registrada para sí misma debe estar CERCANA a sí misma, pero es posible que experimente un poco más de errores de coma flotante debido al algoritmo en cuestión, por lo que solo debe verificar que cada píxel esté dentro de +/- 5% del rango que pueden tomar los píxeles. (0-255 es escala de grises, común en el procesamiento de imágenes). El resultado debe ser al menos del mismo tamaño que la entrada.

Incluso puede simplemente hacer una prueba de humo (es decir, llamarlo y asegurarse de que no se cuelgue). En general, esta técnica es mejor para pruebas más grandes donde el resultado final no puede calcularse (fácilmente) a priori para ejecutar la prueba.

4) Use O ALMACENE una semilla de número aleatorio para su RNG.

Carreras no tienen que ser reproducibles. Sin embargo, es falso que la única forma de obtener una ejecución reproducible es proporcionar una semilla específica a un generador de números aleatorios. A veces las pruebas de aleatoriedad son valiosas. He visto / escuchado sobre errores en el código científico que surgen en casos degenerados que se generaron aleatoriamente (en algoritmos complicados puede ser difícil ver cuál es el caso degenerado incluso) En lugar de llamar siempre a su función con la misma semilla, genere una semilla aleatoria, luego use esa semilla y registre el valor de la semilla. De esa manera, cada ejecución tiene una semilla aleatoria diferente, pero si se bloquea, puede volver a ejecutar el resultado utilizando la semilla que ha registrado para depurar. De hecho, he usado esto en la práctica y aplastó un error, así que pensé en mencionarlo. Es cierto que esto solo sucedió una vez, y estoy seguro de que no siempre vale la pena hacerlo, así que usa esta técnica con prudencia. Sin embargo, el azar con la misma semilla siempre es seguro. Desventaja (en lugar de usar la misma semilla todo el tiempo): debe registrar sus ejecuciones de prueba. Al revés: corrección y eliminación de errores.

Su caso particular

1) Probar que un vacío taskArray devuelve 0 (afirmación conocida).

2) Generar entrada aleatoria de tal manera que task.time > 0 , task.due > 0, y task.importance > 0 para todos los task s, y afirmar el resultado es mayor que 0 (assert áspero, de entrada al azar) . No necesita volverse loco y generar semillas al azar, su algoritmo simplemente no es lo suficientemente complejo como para justificarlo. Hay alrededor de 0 posibilidades de que valga la pena: simplemente mantenga la prueba simple.

3) Probar si task.importance == 0 para todos los task s, luego el resultado es 0 (afirmación conocida)

4) Otras respuestas tocaron esto, pero podría ser importante para su caso particular : si está haciendo que una API sea consumida por usuarios fuera de su equipo, debe probar los casos degenerados. Por ejemplo, si workPerDay == 0, asegúrese de lanzar un error encantador que le indique al usuario que la entrada no es válida. Si no está haciendo una API, y es solo para usted y su equipo, probablemente puede omitir este paso y simplemente negarse a llamarlo con el caso degenerado.

HTH.

Matt Messersmith
fuente
1

Incorpore pruebas de afirmación en su conjunto de pruebas unitarias para pruebas basadas en propiedades de su algoritmo. Además de escribir pruebas unitarias que verifican la salida específica, escriba pruebas diseñadas para fallar al desencadenar fallas de aserción en el código principal.

Muchos algoritmos confían en sus pruebas de corrección para mantener ciertas propiedades a lo largo de las etapas del algoritmo. Si puede verificar con sensatez estas propiedades observando el resultado de una función, la prueba unitaria por sí sola es suficiente para probar sus propiedades. De lo contrario, las pruebas basadas en aserciones le permiten probar que una implementación mantiene una propiedad cada vez que el algoritmo la asume.

Las pruebas basadas en afirmaciones expondrán fallas de algoritmos, errores de codificación y fallas de implementación debido a problemas como la inestabilidad numérica. Muchos lenguajes tienen mecanismos que afirman las aserciones en tiempo de compilación o antes de que se interprete el código, de modo que cuando se ejecutan en modo de producción las aserciones no incurran en una penalización de rendimiento. Si su código pasa las pruebas unitarias pero falla en un caso de la vida real, puede volver a activar las afirmaciones como una herramienta de depuración.

Tobias Hagge
fuente
1

Algunas de las otras respuestas aquí son muy buenas:

  • Base de prueba, bordes y esquinas
  • Realizar controles de cordura
  • Realizar pruebas comparativas

... Añadiría algunas otras tácticas:

  • Descomponer el problema.
  • Probar el algoritmo fuera del código.
  • Pruebe que el algoritmo [probado externamente] se implemente como está diseñado.

La descomposición le permite asegurarse de que los componentes de su algoritmo hagan lo que espera que hagan. Y una descomposición "buena" también le permite asegurarse de que estén bien pegados. Una gran descomposición generaliza y simplifica el algoritmo en la medida en que puede predecir los resultados (de los algoritmos genéricos simplificados) a mano lo suficientemente bien como para escribir pruebas exhaustivas.

Si no puede descomponerse hasta ese punto, pruebe el algoritmo fuera del código por cualquier medio que sea suficiente para satisfacerlo a usted y a sus pares, partes interesadas y clientes. Y luego, descomponga lo suficiente como para demostrar que su implementación coincide con el diseño.

svidgen
fuente
0

Esto puede parecer una respuesta idealista, pero ayuda a identificar diferentes tipos de pruebas.

Si las respuestas estrictas son importantes para la implementación, entonces los ejemplos y las respuestas esperadas realmente deberían proporcionarse en los requisitos que describen el algoritmo. Estos requisitos deben revisarse en grupo y si no obtiene los mismos resultados, debe identificarse el motivo.

Incluso si desempeña el papel de analista e implementador, en realidad debe crear requisitos y hacer que se revisen mucho antes de escribir las pruebas unitarias, por lo que en este caso sabrá los resultados esperados y podrá escribir sus pruebas en consecuencia.

Por otro lado, si esta es una parte que está implementando y que no forma parte de la lógica de negocios o admite una respuesta de lógica de negocios, entonces debería estar bien ejecutar la prueba para ver cuáles son los resultados y luego modificar la prueba para esperar esos resultados Los resultados finales ya se verifican según sus requisitos, por lo que si son correctos, todo el código que alimenta esos resultados finales debe ser numéricamente correcto y en ese punto las pruebas de su unidad son más para detectar casos de falla de borde y futuros cambios de refactorización que para demostrar que un determinado El algoritmo produce resultados correctos.

Bill K
fuente
0

Creo que es perfectamente aceptable en ocasiones seguir el proceso:

  • diseñar un caso de prueba
  • usa tu software para obtener la respuesta
  • verifica la respuesta a mano
  • escriba una prueba de regresión para que las futuras versiones del software continúen dando esta respuesta.

Este es un enfoque razonable en cualquier situación en la que verificar la corrección de una respuesta a mano es más fácil que calcular la respuesta a mano desde los primeros principios.

Conozco personas que escriben software para renderizar páginas impresas y tienen pruebas que verifican que se hayan configurado exactamente los píxeles correctos en la página impresa. La única forma sensata de hacerlo es escribir el código para representar la página, comprobar a simple vista que se ve bien y luego capturar el resultado como una prueba de regresión para futuras versiones.

El hecho de que lea en un libro que una metodología en particular alienta a escribir los casos de prueba primero, no significa que siempre tenga que hacerlo de esa manera. Las reglas están para romperse.

Michael Kay
fuente
0

Las respuestas de otras respuestas ya tienen técnicas para el aspecto de una prueba cuando el resultado específico no se puede determinar fuera de la función probada.

Lo que hago además, que no he visto en las otras respuestas, es generar automáticamente pruebas de alguna manera:

  1. Entradas 'aleatorias'
  2. Iteración a través de rangos de datos
  3. Construcción de casos de prueba a partir de conjuntos de límites.
  4. Todo lo anterior.

Por ejemplo, si la función toma tres parámetros, cada uno con el rango de entrada permitido [-1,1], pruebe todas las combinaciones de cada parámetro, {-2, -1.01, -1, -0.99, -0.5, -0.01, 0,0.01 , 0,5,0,99,1,1.01,2, algo más aleatorio en (-1,1)}

En resumen: a veces la mala calidad puede ser subsidiada por la cantidad

Keith
fuente