Cómo lidiar con la prueba pasando desde el principio en TDD

8

Estoy tratando de practicar TDD en mi proyecto personal y me pregunto cómo lidiar con la situación cuando, después de agregar una nueva prueba, se pasa desde el principio en función de la implementación existente.

Por un lado, la nueva prueba puede proporcionar documentación adicional del diseño y una protección contra la violación accidental de supuestos.

Por otro lado, si la prueba pasa sin ningún cambio de código, entonces es "sospechoso" si realmente prueba o no lo que debería.

Básicamente, ¿qué se puede hacer para confirmar la corrección de la prueba que afirma el comportamiento ya implementado?

AGrzes
fuente
55
mi enfoque preferido es la prueba de mutación, cambio el código probado y comprobar si el examen comience su defecto
mosquito
2
@gnat: creo que hay cierta superposición, pero no creo que sea la misma pregunta. Aquí estoy preguntando específicamente sobre TDD y creo que "si te encuentras en tal situación, entonces estás haciendo TDD mal porque ..." sería una respuesta válida.
AGrzes
55
no importa porque ya no estás haciendo TDD cuando escribes una prueba contra una implementación ya existente (y funciona correctamente)
mosquito
2
"¿Y qué haces cuando te encuentras con eso?" Ejecuto la nueva prueba (solo esto) con una herramienta de cobertura y verifico que se ejecute la ruta esperada. Retroceder es solo una opción si realiza el proyecto de capacitación. Por cierto: el Día Mundial de Coderetreat es una gran oportunidad para practicar TDD ...
Timothy Truckle
2
@ Solomonoff'sSecret personalmente, no confío en ninguna prueba que no haya visto fallar.
RubberDuck

Respuestas:

13

Pasar una prueba desde el momento en que la escribió podría ser un problema con su proceso de TDD, pero no significa que esté mal por sí sola.

Su prueba puede pasar por coincidencia.

Digamos que tiene un método que devuelve el costo de retirar dinero usando un cajero automático. Ahora se le pide que agregue la nueva regla de que si el propietario de la tarjeta tiene más de 60 años, el costo es 0. Entonces lo probamos, esperando que falle:

assertTrue(ATM.withdrawalCost(clientOver60) == 0)

Puede esperar que esto falle. Pero pasa, ya que el cliente resulta ser un cliente VIP, que tiene retiros gratuitos. Ahora puede volver al método retiroCost y modificarlo para que falle, pero eso no tiene mucho sentido. Escriba una nueva prueba para mostrar que su código está mal:

assertTrue(ATM.withdrawalCost(nonVIPclientOver60) == 0)

Ahora falla, ve y codifica hasta que pase, luego repite hasta que termines.

¿Debería borrar la prueba entonces, ya que no hace ninguna diferencia? ¡No! Describe la funcionalidad esperada del método ATM RetireCost. Si lo borra y algún día cambia el costo de retiro de costo 0 para clientes VIP, la primera afirmación aún debería ser cierta.

Dicho esto, para una TDD adecuada, no debe codificar las cosas antes de sus pruebas y luego probar las cosas que sabe que pasará. No considero que este sea el caso por el que estás preguntando.

Creo que el ciclo de aprobación del código de falla está destinado a evitar el desarrollo impulsado por "escribiré estas 3 pruebas que fallarán y estas 2 que pasarán porque ya lo codifiqué sabiendo lo que iba a ser la prueba". Debe saber que algunas personas podrían sentir lo contrario. Escuche sus razones, también podrían ser válidas.

Maximiliano
fuente
10

Por lo general, las pruebas que pasan desde el principio se producen cuando uno implementa algo de una manera más general de lo que realmente se necesita para las pruebas en cuestión. Esto es bastante normal : las pruebas unitarias solo pueden proporcionar un número pequeño y finito de valores de entrada para una función determinada, pero la mayoría de las funciones se escriben para una amplia gama de posibles valores de entrada. A menudo, una implementación diseñada específicamente para los casos de prueba actuales sería más complicada que una solución más general. Si ese es el caso, sería engorroso y propenso a errores diseñar artificialmente el código de una manera que funcione solo para los casos de prueba y falle por todo lo demás.

Por ejemplo, supongamos que necesita una función para devolver el mínimo de algunos valores de una matriz determinada. Realizó una implementación, impulsada por una prueba con una matriz que contiene solo uno o dos valores. Pero en lugar de implementar esto de una manera enrevesada haciendo las comparaciones en diferentes elementos (tal vez solo los primeros dos elementos), llama a una función de mínimo de matriz de la biblioteca estándar de su ecosistema de idiomas y así hace que la implementación sea de una sola línea . Cuando ahora decide agregar una prueba con una matriz de cinco elementos, la prueba probablemente pasará desde el principio.

Pero, ¿cómo sabes que la prueba no es "verde" debido a un error en la prueba misma? Una forma simple y directa de abordar esto es haciendo una modificación temporal al sujeto bajo prueba para que la prueba falle. Por ejemplo, podría agregar intencionalmente una línea if (array.size()==5) return 123a su función. Ahora su prueba de cinco elementos fallará, así que ya sabe

  • la prueba se ejecuta
  • se ejecuta la llamada de afirmación en la prueba
  • la llamada de afirmación en la prueba valida lo correcto

lo que debería darle cierta confianza en la prueba. Después de que haya visto que la prueba falla, deshaga la modificación y la prueba debe pasar nuevamente.

Alternativamente, puede modificar el resultado esperado de una prueba: digamos que su prueba de aprobación contiene una afirmación como

 int result = Subject.UnderTest(...);
 Assert.AreEqual(1,result);

entonces puede editar la prueba y reemplazar el "1" por "2". Cuando la prueba falla (como se esperaba), sabe que funciona como debería y puede deshacer el reemplazo y ver si la prueba ahora se vuelve verde. El riesgo de introducir un error en la prueba mediante este tipo de reemplazo es muy pequeño, por lo que probablemente sea aceptable para la mayoría de los casos del mundo real.

Una forma diferente, tal vez discutible, es establecer un punto de interrupción en la prueba y usar un depurador para avanzar. Eso también debería darle cierta confianza en que el código de prueba se ejecute realmente, y le da la posibilidad de validar la ruta a través de la prueba mediante inspección paso a paso. Sin embargo, se debe tener mucho cuidado de no pasar por alto los errores en una ruta de código específicamente para una prueba fallida. Para pruebas complejas, puede considerar hacer ambas cosas: hacer que falle artificialmente y usar un depurador para inspeccionarlo.

Doc Brown
fuente