Digamos que estás escribiendo un estilo TDD de juego Yahtzee. Desea probar la parte del código que determina si un conjunto de cinco tiradas de dados es o no una casa completa. Hasta donde sé, cuando haces TDD, sigues estos principios:
- Escribe las pruebas primero
- Escribe lo más simple posible que funcione
- Refinar y refactorizar
Entonces, una prueba inicial podría verse así:
public void Returns_true_when_roll_is_full_house()
{
FullHouseTester sut = new FullHouseTester();
var actual = sut.IsFullHouse(1, 1, 1, 2, 2);
Assert.IsTrue(actual);
}
Al seguir "Escribe lo más simple posible que funcione", ahora debes escribir el IsFullHouse
método de esta manera:
public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
{
return true;
}
return false;
}
Esto da como resultado una prueba verde pero la implementación está incompleta.
¿Debería realizar una prueba unitaria de todas las combinaciones válidas posibles (tanto de valores como de posiciones) para una casa completa? Esa parece ser la única forma de estar absolutamente seguro de que su IsFullHouse
código está completamente probado y correcto, pero también parece bastante loco hacerlo.
¿Cómo probarías la unidad algo como esto?
Actualizar
Erik y Kilian señalan que el uso de literales en la implementación inicial para obtener una prueba verde podría no ser la mejor idea. Me gustaría explicar por qué hice eso y esa explicación no cabe en un comentario.
Mi experiencia práctica con las pruebas unitarias (especialmente usando un enfoque TDD) es muy limitada. Recuerdo haber visto una grabación de TDD Masterclass de Roy Osherove en Tekpub. En uno de los episodios construye un estilo TDD de Calculadora de Cuerdas. La especificación completa de la calculadora de cadenas se puede encontrar aquí: http://osherove.com/tdd-kata-1/
Comienza con una prueba como esta:
public void Add_with_empty_string_should_return_zero()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("");
Assert.AreEqual(0, result);
}
Esto da como resultado esta primera implementación del Add
método:
public int Add(string input)
{
return 0;
}
Luego se agrega esta prueba:
public void Add_with_one_number_string_should_return_number()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("1");
Assert.AreEqual(1, result);
}
Y el Add
método se refactoriza:
public int Add(string input)
{
if (input.Length == 0)
{
return 0;
}
return 1;
}
Después de cada paso, Roy dice "Escribe lo más simple que funcionará".
Así que pensé en probar este enfoque al intentar hacer un juego Yahtzee estilo TDD.
fuente
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Respuestas:
Ya hay muchas buenas respuestas a esta pregunta, y he comentado y votado varias de ellas. Aún así, me gustaría agregar algunos pensamientos.
La flexibilidad no es para principiantes
El OP dice claramente que no tiene experiencia con TDD, y creo que una buena respuesta debe tenerlo en cuenta. En la terminología del modelo Dreyfus de adquisición de habilidades , es probable que sea un novato . No hay nada de malo en ser un novato: todos somos novatos cuando comenzamos a aprender algo nuevo. Sin embargo, lo que explica el modelo Dreyfus es que los principiantes se caracterizan por
Esa no es una descripción de una deficiencia de la personalidad, por lo que no hay razón para avergonzarse de eso: es una etapa que todos debemos atravesar para aprender algo nuevo.
Esto también es cierto para TDD.
Si bien estoy de acuerdo con muchas de las otras respuestas aquí, que TDD no tiene que ser dogmático, y que a veces puede ser más beneficioso trabajar de una manera alternativa, eso no ayuda a nadie que recién comienza. ¿Cómo puedes ejercer un juicio discrecional cuando no tienes experiencia?
Si un novato acepta el consejo de que a veces está bien no hacer TDD, ¿cómo puede determinar cuándo está bien dejar de hacer TDD?
Sin experiencia ni orientación, lo único que puede hacer un novato es saltarse el TDD cada vez que se vuelve demasiado difícil. Esa es la naturaleza humana, pero no es una buena forma de aprender.
Escucha las pruebas
Saltar de TDD cada vez que se vuelve difícil es perder uno de los beneficios más importantes de TDD. Las pruebas proporcionan retroalimentación temprana sobre la API del SUT. Si la prueba es difícil de escribir, es una señal importante de que el SUT es difícil de usar.
Esta es la razón por la cual uno de los mensajes más importantes de GOOS es: ¡ escuche sus pruebas!
En el caso de esta pregunta, mi primera reacción al ver la API propuesta del juego Yahtzee, y la discusión sobre combinatoria que se puede encontrar en esta página, fue que esta es una retroalimentación importante sobre la API.
¿La API tiene que representar tiradas de dados como una secuencia ordenada de enteros? Para mí, ese olor a obsesión primitiva . Es por eso que estaba feliz de ver la respuesta de tallseth sugiriendo la introducción de una
Roll
clase. Creo que es una excelente sugerencia.Sin embargo, creo que algunos de los comentarios a esa respuesta se equivocan. Lo que TDD sugiere es que una vez que se tiene la idea de que una
Roll
clase sería una buena idea, suspendes el trabajo en el SUT original y comienzas a trabajar en TDD en laRoll
clase.Si bien estoy de acuerdo en que TDD está más dirigido a la "ruta feliz" que a una prueba exhaustiva, todavía ayuda a dividir el sistema en unidades manejables. Una
Roll
clase suena como algo que podría TDD para completar mucho más fácilmente.Luego, una vez que la
Roll
clase haya evolucionado lo suficiente, ¿volverías al SUT original y lo desarrollarías en términos deRoll
entradas?La sugerencia de un Test Helper no necesariamente implica aleatoriedad, es solo una forma de hacer que la prueba sea más legible.
Otra forma de abordar y modelar la entrada en términos de
Roll
instancias sería introducir un generador de datos de prueba .Rojo / Verde / Refactor es un proceso de tres etapas.
Si bien estoy de acuerdo con el sentimiento general de que (si tiene suficiente experiencia en TDD), no necesita apegarse a TDD rigurosamente, creo que es un consejo bastante pobre en el caso de un ejercicio de Yahtzee. Aunque no conozco los detalles de las reglas de Yahtzee, no veo ningún argumento convincente aquí de que no se pueda seguir rigurosamente con el proceso Rojo / Verde / Refactor y aún así llegar a un resultado adecuado.
Lo que la mayoría de la gente parece olvidar aquí es la tercera etapa del proceso Rojo / Verde / Refactorizador. Primero escribes el examen. Luego, escribe la implementación más simple que pasa todas las pruebas. Entonces refactorizas.
Es aquí, en este tercer estado, donde puedes poner en práctica todas tus habilidades profesionales. Aquí es donde se le permite reflexionar sobre el código.
Sin embargo, creo que es un error decir que solo debe "escribir lo más simple posible que no sea completamente mental y obviamente incorrecto que funcione". Si (cree) que sabe lo suficiente sobre la implementación de antemano, entonces todo lo que no sea la solución completa será obviamente incorrecto . En lo que respecta al consejo, entonces, esto es bastante inútil para un novato.
Lo que realmente debería suceder es que si puede hacer que todas las pruebas pasen con una implementación obviamente incorrecta , eso es retroalimentación de que debe escribir otra prueba .
Es sorprendente la frecuencia con la que eso lo lleva a una implementación completamente diferente a la que tenía en mente primero. A veces, la alternativa que crece así puede ser mejor que su plan original.
El rigor es una herramienta de aprendizaje.
Tiene mucho sentido seguir procesos rigurosos como Rojo / Verde / Refactorio mientras uno esté aprendiendo. Obliga al alumno a adquirir experiencia con TDD no solo cuando es fácil, sino también cuando es difícil.
Solo cuando haya dominado todas las partes difíciles estará en condiciones de tomar una decisión informada sobre cuándo desviarse del camino "verdadero". Ahí es cuando comienzas a formar tu propio camino.
fuente
Como descargo de responsabilidad, esto es TDD mientras lo practico y, como Kilian señala acertadamente, desconfiaría de cualquiera que sugiriera que hay una forma correcta de practicarlo. Pero tal vez te ayude ...
En primer lugar, lo más simple que podría hacer para que su examen sea aprobado sería este:
Esto es significativo porque no se debe a alguna práctica de TDD, sino porque la grabación en todos esos literales no es realmente una buena idea. Una de las cosas más difíciles de entender con TDD es que no es una estrategia de prueba integral, es una forma de protegerse contra las regresiones y marcar el progreso, manteniendo el código simple. Es una estrategia de desarrollo y no una estrategia de prueba.
La razón por la que menciono esta distinción es que ayuda a guiar las pruebas que debe escribir. La respuesta a "¿qué pruebas debo escribir?" es "cualquier prueba que necesite para obtener el código de la manera que lo desee". Piense en TDD como una forma de ayudarlo a descubrir algoritmos y razonar sobre su código. Entonces, dada su prueba y mi implementación "verde simple", ¿qué prueba viene después? Bueno, has establecido algo que es una casa llena, entonces, ¿cuándo no es una casa llena?
Ahora tiene que encontrar alguna manera de diferenciar entre los dos casos de prueba que sea significativo . Personalmente, agregaría un poco de información aclaratoria para "hacer lo más simple para aprobar la prueba" y decir "hacer lo más simple para aprobar la prueba que fomenta su implementación". Escribir pruebas fallidas es su pretexto para alterar el código, así que cuando vaya a escribir cada prueba, debería preguntarse "¿qué no hace mi código que quiero que haga y cómo puedo exponer esa deficiencia?" También puede ayudarlo a fortalecer su código y a manejar casos extremos. ¿Qué haces si una persona que llama no tiene sentido?
En resumen, si está probando cada combinación de valores, es casi seguro que lo está haciendo mal (y es probable que termine con una explosión combinatoria de condicionales). Cuando se trata de TDD, debe escribir la cantidad mínima de casos de prueba necesarios para obtener el algoritmo que desea. Cualquier otra prueba que escriba comenzará en verde y, por lo tanto, se convertirá en documentación, en esencia, y no estrictamente parte del proceso de TDD. Solo escribirá más casos de prueba TDD si los requisitos cambian o se expone un error, en cuyo caso documentará la deficiencia con una prueba y luego la aprobará.
Actualizar:
Comencé esto como un comentario en respuesta a su actualización, pero comenzó a ser bastante largo ...
Yo diría que el problema no es con la existencia de literales, punto, sino con lo 'más simple' que es un condicional de 5 partes. Cuando lo piensas, un condicional de 5 partes es bastante complicado. Será común usar literales durante el paso de rojo a verde y luego abstraerlos a constantes en el paso de refactorización o generalizarlos en una prueba posterior.
Durante mi propio viaje con TDD, me di cuenta de que hay que hacer una distinción importante: no es bueno confundir "simple" y "obtuso". Es decir, cuando comencé, vi a las personas hacer TDD y pensé "simplemente están haciendo lo más tonto posible para que las pruebas pasen" y lo imité por un tiempo, hasta que me di cuenta de que "simple" era sutilmente diferente que "obtuso". A veces se superponen, pero a menudo no.
Entonces, disculpas si daba la impresión de que la existencia de literales era el problema, no lo es. Yo diría que la complejidad del condicional con las 5 cláusulas es el problema. Su primer rojo a verde puede ser simplemente "volver verdadero" porque es realmente simple (y obtuso, por coincidencia). El próximo caso de prueba, con (1, 2, 3, 4, 5) tendrá que devolver falso, y aquí es donde comienza a dejar atrás "obtuso". Tiene que preguntarse "¿por qué es (1, 1, 1, 2, 2) una casa llena y (1, 2, 3, 4, 5) no lo es?" Lo más simple que se te ocurre es que uno tiene el último elemento de secuencia 5 o el segundo elemento de secuencia 2 y el otro no. Esos son simples, pero también son (innecesariamente) obtusos. Lo que realmente quieres conducir es "¿cuántos tienen el mismo número?" Por lo tanto, puede pasar la segunda prueba comprobando si hay una repetición o no. En el que tiene una repetición, tienes una casa llena, y en el otro no. Ahora la prueba pasa y usted escribe otro caso de prueba que tiene una repetición pero no es una casa completa para refinar aún más su algoritmo.
Puede o no hacer esto con literales a medida que avanza, y está bien si lo hace. Pero la idea general es hacer crecer su algoritmo 'orgánicamente' a medida que agrega más casos.
fuente
La prueba de cinco valores literales particulares en una combinación particular no es "lo más simple" para mi cerebro con fiebre. Si la solución a un problema es realmente obvia (cuente si tiene exactamente tres y exactamente dos de algún valor), entonces siga adelante y codifique esa solución, y escriba algunas pruebas que sería muy, muy poco probable que satisfagan accidentalmente con la cantidad de código que escribió (es decir, diferentes literales y diferentes órdenes de triples y dobles).
Las máximas de TDD son realmente herramientas, no creencias religiosas. Su objetivo es lograr que escriba código correcto y bien factorizado rápidamente. Si una máxima obviamente se interpone en el camino de eso, simplemente salte adelante y continúe con el siguiente paso. Habrá muchos bits no obvios en su proyecto donde podrá aplicarlo.
fuente
La respuesta de Erik es genial, pero pensé que podría compartir un truco en la escritura de prueba.
Comience con esta prueba:
Esta prueba es aún mejor si crea una
Roll
clase en lugar de pasar 5 parámetros:Eso le da a esta implementación:
Luego escribe esta prueba:
Una vez que eso esté pasando, escribe este:
Después de eso, apuesto a que no necesitas escribir más (tal vez dos pares, o tal vez yahtzee, si crees que no es una casa llena).
Obviamente, implemente sus métodos Any para devolver Rolls aleatorios que cumplan con sus criterios.
Hay algunos beneficios en este enfoque:
fuente
IsFullHouse
Realmente debería volvertrue
sipairNum == trioNum
?Puedo pensar en dos formas principales que consideraría al probar esto;
Agregue "algunos" más casos de prueba (~ 5) de conjuntos válidos de full-house, y la misma cantidad de falsificaciones esperadas ({1, 1, 2, 3, 3} es buena. Recuerde que, por ejemplo, 5 podrían ser reconocido como "3 de lo mismo más un par" por una implementación incorrecta). Este método asume que el desarrollador no solo está tratando de pasar las pruebas, sino que realmente lo implementa correctamente.
Prueba todos los juegos de dados posibles (solo hay 252 diferentes). Por supuesto, esto supone que tiene alguna forma de saber cuál es la respuesta esperada (al probar esto se conoce como una
oracle
). Esto podría ser una implementación de referencia de la misma función, o un ser humano. Si desea ser realmente riguroso, podría valer la pena codificar manualmente cada resultado esperado.De hecho, una vez escribí una IA de Yahtzee, que por supuesto tenía que conocer las reglas. Puede encontrar el código para la parte de evaluación de puntaje aquí , tenga en cuenta que la implementación es para la versión escandinava (Yatzy), y nuestra implementación asume que los dados se dan en orden ordenado.
fuente
Este ejemplo realmente pierde el punto. Estamos hablando de una única función sencilla aquí, no un diseño de software. ¿Es un poco complicado? Sí, entonces lo descomponen. Y absolutamente no prueba todas las entradas posibles de 1, 1, 1, 1, 1 a 6, 6, 6, 6, 6, 6. La función en cuestión no requiere orden, solo una combinación, a saber, AAABB.
No necesita 200 pruebas lógicas separadas. Podría usar un conjunto, por ejemplo. Casi cualquier lenguaje de programación tiene uno incorporado:
Y si recibe una entrada que no es una tirada válida de Yahtzee, debe lanzar como si no hubiera un mañana.
fuente