Al hacer TDD y escribir una prueba unitaria, ¿cómo se resiste el impulso de "hacer trampa" al escribir la primera iteración del código de "implementación" que está probando?
Por ejemplo:
necesito calcular el factorial de un número. Comienzo con una prueba unitaria (usando MSTest) algo como:
[TestClass]
public class CalculateFactorialTests
{
[TestMethod]
public void CalculateFactorial_5_input_returns_120()
{
// Arrange
var myMath = new MyMath();
// Act
long output = myMath.CalculateFactorial(5);
// Assert
Assert.AreEqual(120, output);
}
}
Ejecuto este código y falla ya que el CalculateFactorial
método ni siquiera existe. Entonces, ahora escribo la primera iteración del código para implementar el método bajo prueba, escribiendo el código mínimo requerido para pasar la prueba.
La cuestión es que continuamente tengo la tentación de escribir lo siguiente:
public class MyMath
{
public long CalculateFactorial(long input)
{
return 120;
}
}
Esto es técnicamente correcto, ya que realmente es el código mínimo requerido para hacer que esa prueba específica pase (ir verde), aunque es claramente un "truco" ya que realmente ni siquiera intenta realizar la función de calcular un factorial. Por supuesto, ahora la parte de refactorización se convierte en un ejercicio para "escribir la funcionalidad correcta" en lugar de una verdadera refactorización de la implementación. Obviamente, agregar pruebas adicionales con diferentes parámetros fallará y forzará una refactorización, pero debe comenzar con esa prueba.
Entonces, mi pregunta es, ¿cómo logras ese equilibrio entre "escribir el código mínimo para pasar la prueba" mientras lo mantienes funcional y en el espíritu de lo que realmente estás tratando de lograr?
fuente
Respuestas:
Es perfectamente legítimo. Rojo, verde, refactor.
La primera prueba pasa.
Agregue la segunda prueba, con una nueva entrada.
Ahora llegue rápidamente al verde, puede agregar un if-else, que funciona bien. Pasa, pero aún no has terminado.
La tercera parte de Red, Green, Refactor es la más importante. Refactorizar para eliminar la duplicación . Tendrás duplicación en tu código ahora. Dos declaraciones que devuelven enteros. Y la única forma de eliminar esa duplicación es codificar la función correctamente.
No digo que no lo escribas correctamente la primera vez. Solo digo que no es trampa si no lo haces.
fuente
Claramente, se requiere una comprensión del objetivo final y el logro de un algoritmo que cumpla ese objetivo.
TDD no es una bala mágica para el diseño; todavía tiene que saber cómo resolver problemas usando el código, y aún debe saber cómo hacerlo a un nivel superior a unas pocas líneas de código para pasar una prueba.
Me gusta la idea de TDD porque fomenta el buen diseño; te hace pensar en cómo puedes escribir tu código para que sea comprobable y, en general, esa filosofía empujará el código hacia un mejor diseño en general. Pero aún debe saber cómo diseñar una solución.
No estoy a favor de las filosofías reduccionistas de TDD que afirman que puede hacer crecer una aplicación simplemente escribiendo la menor cantidad de código para aprobar una prueba. Sin pensar en la arquitectura, esto no funcionará, y su ejemplo lo demuestra.
El tío Bob Martin dice esto:
fuente
Una muy buena pregunta ... y tengo que estar en desacuerdo con casi todos excepto @Robert.
Escritura
para que una función factorial pase una prueba es una pérdida de tiempo . No es "trampa", ni está siguiendo al refactor rojo-verde literalmente. Es equivocado .
Este es el por qué:
los argumentos 'refactorizadores' están equivocados; Si tiene dos casos de prueba para 5 y 6, este código sigue siendo incorrecto, porque no está calculando un factorial en absoluto :
si seguimos el argumento 'refactor' literalmente , cuando tengamos 5 casos de prueba invocaríamos a YAGNI e implementaríamos la función usando una tabla de búsqueda:
Ninguno de estos realmente está calculando nada, lo eres . ¡Y esa no es la tarea!
fuente
Cuando ha escrito solo una prueba unitaria, la implementación de una línea (
return 120;
) es legítima. Escribir un bucle que calcule el valor de 120, ¡ sería una trampa!Estas pruebas iniciales simples son una buena manera de detectar casos extremos y evitar errores únicos. Cinco en realidad no es el valor de entrada con el que comenzaría.
Una regla general que podría ser útil aquí es: cero, uno, muchos, lotes . Cero y uno son casos importantes para el factorial. Se pueden implementar con líneas simples. El caso de prueba "muchos" (por ejemplo, 5!) Te obligaría a escribir un bucle. El caso de prueba "lotes" (1000 !?) podría obligarlo a implementar un algoritmo alternativo para manejar números muy grandes.
fuente
factorial(5)
es una mala primera prueba. partimos de los casos más simples posibles y en cada iteración hacemos que las pruebas sean un poco más específicas, instando al código a volverse un poco más genérico. esto es lo que Bob tío llama la premisa prioritaria la transformación ( blog.8thlight.com/uncle-bob/2013/05/27/... )Siempre que solo tenga una sola prueba, entonces el código mínimo necesario para aprobar la prueba es verdaderamente
return 120;
, y puede mantenerlo fácilmente mientras no tenga más pruebas.Esto le permite posponer más diseños hasta que realmente escriba las pruebas que ejercitan OTROS valores de retorno de este método.
Recuerde que la prueba es la versión ejecutable de su especificación, y si todo lo que dice esa especificación es que f (6) = 120, entonces eso se ajusta perfectamente.
fuente
Si puede "hacer trampa" de esa manera, sugiere que las pruebas de su unidad son defectuosas.
En lugar de probar el método factorial con un solo valor, pruebe que era un rango de valores. Las pruebas basadas en datos pueden ayudar aquí.
Vea sus pruebas unitarias como una manifestación de los requisitos: deben definir colectivamente el comportamiento del método que prueban. (Esto se conoce como desarrollo impulsado por el comportamiento : es el futuro
;-)
)Entonces pregúntese: si alguien cambiara la implementación a algo incorrecto, ¿pasarían sus pruebas o dirían "espera un minuto"?
Teniendo esto en cuenta, si su única prueba era la que estaba en su pregunta, entonces técnicamente, la implementación correspondiente es correcta. El problema se ve entonces como requisitos mal definidos.
fuente
case
declaraciones a unaswitch
, y no puede escribir una prueba para cada entrada y salida posible para el ejemplo del OP.Int64.MinValue
aInt64.MaxValue
. Llevaría mucho tiempo ejecutarlo, pero definiría explícitamente el requisito sin margen de error. Con la tecnología actual, esto es inviable (sospecho que podría volverse más común en el futuro) y estoy de acuerdo, podría hacer trampa, pero creo que la pregunta de los OP no era práctica (nadie realmente haría trampa de tal manera en la práctica), pero teórica.Solo escribe más pruebas. Eventualmente, sería más corto escribir
que
:-)
fuente
Escribir pruebas de "trampa" está bien, para valores suficientemente pequeños de "OK". Pero recuerde: las pruebas unitarias solo se completan cuando todas las pruebas pasan y no se pueden escribir nuevas pruebas que fallen . Si realmente desea tener un método CalculateFactorial que contenga un montón de declaraciones if (o incluso mejor, una gran declaración de cambio / caso :-) puede hacerlo, y dado que se trata de un número de precisión fija, el código requerido implementar esto es finito (aunque probablemente bastante grande y feo, y tal vez limitado por las limitaciones del compilador o del sistema en el tamaño máximo del código de un procedimiento). En este punto si realmenteinsista en que todo el desarrollo debe ser impulsado por una prueba unitaria; puede escribir una prueba que requiera que el código calcule el resultado en un período de tiempo más corto que el que se puede lograr siguiendo todas las ramas de la instrucción if .
Básicamente, TDD puede ayudarlo a escribir código que implemente los requisitos correctamente , pero no puede obligarlo a escribir un buen código. Eso depende de usted.
Comparte y Disfruta.
fuente
Estoy 100% de acuerdo con la sugerencia de Robert Harvey aquí, no se trata solo de aprobar las pruebas, también debes tener en cuenta el objetivo general.
Como solución a su punto de dolor de "solo se verifica trabajar con un conjunto dado de entradas", propondría usar pruebas basadas en datos, como la teoría de xunit. El poder detrás de este concepto es que le permite crear fácilmente especificaciones de entradas a salidas.
Para Factorials, una prueba se vería así:
Incluso podría implementar un suministro de datos de prueba (que devuelve
IEnumerable<Tuple<xxx>>
) y codificar una invariante matemática, como dividir repetidamente entre n producirá n-1).Creo que este tp es una forma muy poderosa de prueba.
fuente
Si aún puede hacer trampa, entonces las pruebas no son suficientes. Escribe más pruebas! Para su ejemplo, intentaré agregar pruebas con la entrada 1, -1, -1000, 0, 10, 200.
Sin embargo, si realmente te comprometes a hacer trampa, puedes escribir un interminable si-entonces. En este caso, nada podría ayudar excepto la revisión de código. Pronto sería atrapado en la prueba de aceptación (¡ escrito por otra persona! )
El problema con las pruebas unitarias es que a veces los programadores las consideran un trabajo innecesario. La forma correcta de verlos es como una herramienta para que usted haga el resultado correcto de su trabajo. Entonces, si creas un if-then, sabes inconscientemente que hay otros casos a considerar. Esto significa que tienes que escribir otras pruebas. Y así sucesivamente hasta que te des cuenta de que el engaño no funciona y es mejor simplemente codificar de la manera correcta. Si todavía siente que no ha terminado, no ha terminado.
fuente
Sugeriría que su elección de prueba no es la mejor prueba.
Yo comenzaría con:
factorial (1) como la primera prueba,
factorial (0) como el segundo
factorial (-ve) como el tercero
y luego continuar con casos no triviales
y terminar con un caso de desbordamiento.
fuente
-ve
??