¿Cómo funciona TDD cuando puede haber millones de casos de prueba para una funcionalidad de producción?

En TDD, selecciona un caso de prueba e implementa ese caso de prueba, luego escribe suficiente código de producción para que la prueba pase, refactorice los códigos y nuevamente elija un nuevo caso de prueba y el ciclo continúa.

El problema que tengo con este proceso es que TDD dice que usted escribe suficiente código solo para pasar la prueba que acaba de escribir. A lo que me refiero exactamente es que si un método puede tener, por ejemplo, 1 millón de casos de prueba, ¿qué puede hacer usted? Obviamente no se escribe 1 millón de casos de prueba?

Permítanme explicar lo que quiero decir más claramente con el siguiente ejemplo:

internal static List GetPrimeFactors(ulong number) { var result = new List(); while (number % 2 == 0) { result.Add(2); number = number / 2; } var divisor = 3; while (divisor <= number) { if (number % divisor == 0) { result.Add(divisor); number = number / divisor; } else { divisor += 2; } } return result; } 

El código anterior devuelve todos los factores primos de un número dado. ¡Ulong tiene 64 bits, lo que significa que puede aceptar valores entre 0 y 18,446,744,073,709,551,615!

Entonces, ¿cómo funciona TDD cuando puede haber millones de casos de prueba para una funcionalidad de producción?

Me refiero a cuántos casos de prueba son suficientes para ser escritos para poder decir que utilicé TDD para lograr este código de producción.

Este concepto en TDD que dice que solo debe escribir suficiente código para aprobar la prueba, ¿me parece incorrecto, como puede verse en el ejemplo anterior?

Cuando suficiente es suficiente?

Mi opinión es que solo escojo algunos casos de prueba, por ejemplo, para banda superior, banda inferior y algunos más, por ejemplo, 5 casos de prueba, pero eso no es TDD, ¿verdad?

Muchas gracias por sus pensamientos sobre TDD para este ejemplo.

Es una pregunta interesante, relacionada con la idea de falsedad en epistemología. Con las pruebas unitarias, realmente no está intentando probar que el sistema funciona; estás construyendo experimentos que, si fallan, demostrarán que el sistema no funciona de una manera consistente con tus expectativas / creencias. Si sus pruebas pasan, usted no sabe que su sistema funciona, porque puede haber olvidado algún caso de borde que no está probado; lo que sabe es que a partir de ahora, no tiene motivos para creer que su sistema es defectuoso.

El ejemplo clásico en la historia de las ciencias es la pregunta “¿Son todos los cisnes blancos?”. No importa cuántos cisnes blancos diferentes encuentres, no puedes decir que la hipótesis “todos los cisnes son blancos” es correcta. Por otro lado, tráeme un cisne negro y sé que la hipótesis no es correcta.

Una buena prueba de unidad TDD es en este sentido; Si pasa, no te dirá que todo está bien, pero si falla, te dirá dónde está incorrecta tu hipótesis. En ese marco, las pruebas para cada número no son tan valiosas: un caso debería ser suficiente, porque si no funciona en ese caso, sabes que algo está mal.

Sin embargo, donde la pregunta es interesante es que a diferencia de los cisnes, donde realmente no se puede enumerar sobre todos los cisnes del mundo, y todos sus futuros hijos y sus padres, se podría enumerar cada número entero, que es un conjunto finito, y verificar cada situación posible Además, un progtwig está mucho más cerca de las matemáticas que de la física, y en algunos casos también puede verificar realmente si una afirmación es cierta, pero ese tipo de verificación no es, en mi opinión, lo que está buscando la TDD. TDD persigue buenos experimentos que apuntan a capturar posibles casos de fallas, no a probar que algo es cierto.

Estás olvidando el paso tres:

  1. rojo
  2. Verde
  3. Refactor

Escribir tus casos de prueba te pone rojo.

Escribir suficiente código para hacer que esos casos de prueba pasen a verde.

La generalización de su código para que funcione más allá de los casos de prueba que escribió, aunque no rompa ninguno de ellos, es la refactorización.

Parece que estás tratando el TDD como si fuera una prueba de caja negra . No es. Si se tratara de una prueba de caja negra, solo un conjunto completo de pruebas (millones de casos de prueba) podría satisfacerlo, porque cualquier caso dado podría no estar probado, y por lo tanto los demonios en la caja negra podrían salirse con la suya con un tramposo.

Pero no son demonios en la caja negra de tu código. Eres tú, en una caja blanca. Sabes si estás haciendo trampa o no. La práctica de Fake It Til You Make It está estrechamente relacionada con TDD y, a veces, se confunde con ella. Sí, escribes implementaciones falsas para satisfacer los casos de prueba iniciales, pero sabes que lo estás fingiendo. Y también sabes cuándo has dejado de fingirlo. Usted sabe cuándo tiene una implementación real y lo ha logrado mediante iteración progresiva y manejo de prueba.

Así que tu pregunta está realmente fuera de lugar. Para TDD, debe escribir suficientes casos de prueba para llevar a su solución a la finalización y corrección; no necesita casos de prueba para cada conjunto de entradas imaginables.

Desde mi punto de vista, el paso de refactorización no parece haber tenido lugar en este código …

En mi libro, TDD NO significa escribir testcases para cada posible permutación de cada posible parámetro de entrada / salida …

PERO para escribir todos los estuches de prueba necesarios para asegurarse de que hace lo que se especifica que esté haciendo, es decir, para tal método todos los casos de límites más una prueba que selecciona aleatoriamente un número de una lista que contiene números con resultados correctos conocidos. Si es necesario, siempre puede ampliar esta lista para que la prueba sea más completa …

TDD solo funciona en el mundo real si no arrojas el sentido común por la ventana …

En cuanto a

Solo escribe suficiente código para pasar tu examen

en TDD, esto se refiere a “progtwigdores que no hacen trampa” … Si tiene uno o más “progtwigdores de trampa” que, por ejemplo, simplemente codifican el “resultado correcto” de los testcases en el método, sospecho que tiene un problema mucho mayor en su manos que TDD …

Por cierto, la “construcción de estuches de prueba” es algo que mejora a medida que lo practica, no hay ningún libro / guía que pueda decirle cuáles son los mejores estuches para una situación dada desde el principio … la experiencia vale la pena cuando se trata de construirlos. ..

TDD le permite usar el sentido común si lo desea. No tiene sentido que tu versión de TDD sea estúpida, solo para que puedas decir “no estamos haciendo TDD, estamos haciendo algo menos estúpido”.

Puede escribir un solo caso de prueba que llame a la función bajo prueba más de una vez, pasando diferentes argumentos. Esto evita que “escriba código para factorizar 1”, “escriba código para factorizar 2”, “escriba código para factorizar 3” como tareas de desarrollo independientes.

La cantidad de valores distintos a probar realmente depende de cuánto tiempo tenga para ejecutar las pruebas. Desea probar cualquier cosa que pueda ser un caso de esquina (por lo tanto, en el caso de la factorización, al menos 0, 1, 2, 3, LONG_MAX+1 ya que tiene la mayoría de los factores, el valor que tenga la mayoría de los factores, un número de Carmichael, y unos cuantos cuadrados perfectos con varios números de factores primos más el mayor rango de valores posible con la esperanza de cubrir algo de lo que no se dio cuenta fue un caso de esquina, pero lo es. Esto puede significar escribir la prueba, luego escribir la función y luego ajustar el tamaño del rango en función de su rendimiento observado.

También se le permite leer la especificación de la función e implementar la función como si se probaran más valores de los que realmente se verán. Esto realmente no contradice “solo implementa lo que se probó”, solo reconoce que no hay suficiente tiempo antes de la fecha de envío para ejecutar las 2 ^ 64 entradas posibles, por lo que la prueba real es una muestra representativa de la prueba “lógica” que correrías si tuvieras tiempo. Aún puede codificar lo que desea probar, en lugar de lo que realmente tiene tiempo para probar.

Incluso podría probar entradas seleccionadas al azar (comunes como parte de “fuzzing” por los analistas de seguridad), si encuentra que sus progtwigdores (es decir, usted mismo) son perversos, y siga escribiendo código que solo resuelva las entradas probadas, y no otros. Obviamente, hay problemas relacionados con la repetibilidad de las pruebas aleatorias, así que use un PRNG y registre la semilla. Se ve algo similar con la progtwigción de la competencia, los progtwigs de jueces en línea y similares, para evitar trampas. El progtwigdor no sabe exactamente qué entradas se probarán, por lo que debe intentar escribir código que resuelva todas las entradas posibles. Como no puedes ocultarte secretos, las entradas aleatorias hacen el mismo trabajo. En la vida real, los progtwigdores que usan TDD no hacen trampa a propósito, pero pueden hacer trampa accidentalmente porque la misma persona escribe la prueba y el código. Curiosamente, las pruebas pasan por alto los mismos casos difíciles en las esquinas que el código.

El problema es aún más obvio con una función que toma una entrada de cadena, hay más de 2^64 valores de prueba posibles. La elección de los mejores, es decir, el progtwigdor que más probablemente se equivoca, es, en el mejor de los casos, una ciencia inexacta.

También puede dejar que el probador haga trampa, moviéndose más allá de TDD. Primero escriba la prueba, luego escriba el código para pasar la prueba, luego regrese y escriba más pruebas de recuadro blanco, que (a) incluya valores que parezcan ser casos de borde en la implementación realmente escrita; e (b) incluya valores suficientes para obtener una cobertura de código del 100%, para cualquier métrica de cobertura de código en la que tenga el tiempo y la fuerza de voluntad para trabajar. La parte TDD del proceso sigue siendo útil, ayuda a escribir el código, pero luego se repite. Si cualquiera de estas nuevas pruebas falla, podría llamarlo “agregar nuevos requisitos”, en cuyo caso, supongo que lo que está haciendo todavía es TDD puro. Pero es solo una cuestión de cómo lo llama, realmente no está agregando nuevos requisitos, está probando los requisitos originales más exhaustivamente de lo que era posible antes de que se escribiera el código.

Cuando escribes una prueba debes tomar casos significativos , no todos los casos. Los casos significativos incluyen casos generales, casos de esquina …

Simplemente NO PUEDES escribir una prueba para cada caso (de lo contrario, podrías poner los valores en una tabla y responderlos, así estarás 100% seguro de que tu progtwig funcionará: P).

Espero que ayude.

Esa es la primera pregunta que tienes para cualquier prueba. TDD no tiene importancia aquí.

Sí, hay muchos casos; además, hay combinaciones y combinaciones de casos si empiezas a construir el sistema. De hecho, te llevará a una explosión combinatoria.

Qué hacer al respecto es una buena pregunta. Por lo general, elige clases de equivalencia para las que su algoritmo probablemente funcionará igual y prueba un valor para cada clase.

El siguiente paso sería, probar las condiciones de los límites (recuerde, los dos errores más frecuentes en CS están desactivados por un error).

Siguiente … Bueno, por todas las razones prácticas, está bien detenerse aquí. Aún así, eche un vistazo a estas notas de la conferencia: http://www.scs.stanford.edu/11au-cs240h/notes/testing.html

PD. Por cierto, usar TDD “por libro” para problemas matemáticos no es una buena idea. Kent Beck en su libro de TDD lo demuestra, implementando la peor implementación posible de una función que calcula los números de Fibonacci. Si conoce un formulario cerrado, o si tiene un artículo que describe un algoritmo comprobado , simplemente realice las comprobaciones de integridad tal como se describió anteriormente y no realice TDD con todo el ciclo de refactorización, ahorrará tiempo.

PPS. En realidad, hay un buen artículo que (¡sorpresa!) Menciona el problema de Fibonacci y el problema que tiene con TDD.

No hay millones de casos de prueba. Sólo unos pocos. Es posible que desee probar PEX , que le permitirá conocer los diferentes casos de prueba reales en su algoritmo. Por supuesto, solo necesitas probarlos.

Nunca he hecho ningún TDD, pero lo que estás preguntando no es sobre TDD: se trata de cómo escribir un buen conjunto de pruebas.

Me gusta diseñar modelos (en papel o en mi cabeza) de todos los estados en los que puede estar cada pieza de código. Considero cada línea como si fuera parte de una máquina de estados. Para cada una de esas líneas, determino todas las transiciones que se pueden hacer (ejecutar la siguiente línea, bifurcar o no bifurcar, lanzar una excepción, desbordar cualquiera de los cálculos secundarios en la expresión, etc.).

A partir de ahí tengo una matriz básica para mis casos de prueba. Luego, determino cada condición de límite para cada una de esas transiciones de estado, y cualquier punto medio interesante entre cada uno de esos límites. Luego tengo las variaciones para mis casos de prueba.

Desde aquí trato de encontrar combinaciones interesantes y diferentes de flujo o lógica: “Esta statement if, más esa – con varios elementos en la lista”, etc.

Dado que el código es un flujo, a menudo no puede interrumpirlo en el medio a menos que tenga sentido insertar un simulacro para una clase no relacionada. En esos casos, a menudo he reducido mi matriz bastante, porque existen condiciones que simplemente no se pueden alcanzar, o porque la variación se vuelve menos interesante al ser ocultada por otra lógica.

Después de eso, estoy casi cansado por el día y me voy a casa 🙂 Y probablemente tenga entre 10 y 20 casos de prueba por método bien factorizado y razonablemente corto, o entre 50 y 100 por algoritmo / clase. No 10,000,000.

Probablemente se me ocurran demasiados casos de prueba poco interesantes, pero al menos usualmente hago más pruebas en lugar de pruebas. Mitigo esto tratando de factorizar bien mis casos de prueba para evitar la duplicación de código.

Piezas clave aquí:

  • Modele sus algoritmos / objetos / código, al menos en su cabeza. Tu código es más un árbol que un script
  • Determine exhaustivamente todas las transiciones de estado dentro de ese modelo (cada operación se puede ejecutar de forma independiente y cada parte de cada expresión que se evalúa)
  • Utilice la prueba de límites para que no tenga que crear variaciones infinitas
  • Burlarse cuando puedas

Y no, no tiene que escribir dibujos FSM , a menos que se divierta haciendo ese tipo de cosas. Yo no 🙂

Lo que usualmente haces, prueba contra “condiciones de límites de prueba”, y algunas condiciones aleatorias.

por ejemplo: ulong.min, ulong.max y algunos valores. ¿Por qué estás haciendo un GetPrimeFactors? ¿Le gusta calcularlos en general o está haciendo eso para hacer algo específico? Prueba por qué lo estás haciendo.

Lo que también podría hacerlo Afirmar para el resultado. Cuenta, en lugar de todos los elementos individuales. Si sabe cuántos elementos debe obtener y algunos casos específicos, aún puede refactorizar su código y si esos casos y el recuento total son iguales, suponga que la función aún funciona.

Si realmente quiere probar tanto, también podría estudiar las pruebas de caja blanca. Por ejemplo, Pex y Moles es bastante bueno.

TDD no es una forma de verificar que una función / progtwig funcione correctamente en cada permutación de entradas posible. Mi opinión es que la probabilidad de que escriba un caso de prueba particular es proporcional a la incertidumbre que tengo de que mi código sea correcto en ese caso.

Básicamente, esto significa que escribo pruebas en dos escenarios: 1) algún código que he escrito es complicado o complejo y / o tiene demasiadas suposiciones y 2) un error ocurre en la producción

Una vez que entiendes qué causa un error, generalmente es muy fácil de codificar en un caso de prueba. A largo plazo, hacer esto produce un conjunto de pruebas robusto.