¿Qué tan pequeños son tus pequeños pasos en TDD?

37

Hoy estábamos entrenando TDD y encontramos el siguiente punto de malentendido.

La tarea es para la suma de números de retorno de entrada "1,2" que es 3. Lo que he escrito (en C #) fue:

numbers = input.Split(',');
return int.Parse(numbers[0]) + int.Parse(numbers[1]); //task said we have two numbers and input is correct

Pero otros muchachos prefirieron hacerlo de otra manera. Primero, para la entrada "1,2" agregaron el siguiente código:

if (input == "1,2")
   return 3;

Luego introdujeron una prueba más para la entrada "4,5" y cambiaron la implementación:

if (input == "1,2")
   return 3;
else if (input == "4,5")
   return 9;

Y después de eso dijeron "Está bien, ahora vemos el patrón" e implementaron lo que hice inicialmente.

Creo que el segundo enfoque se ajusta mejor a la definición de TDD pero ... ¿deberíamos ser tan estrictos al respecto? Para mí está bien omitir pequeños pasos triviales y combinarlos en "pasos gemelos" si estoy lo suficientemente seguro como para no omitir nada. ¿Me equivoco?

Actualizar. He cometido un error al no aclarar que no era la primera prueba. Ya había algunas pruebas, por lo que "return 3" en realidad no era el código más simple para satisfacer el requisito.

SiberianGuy
fuente
25
Tan pequeño que mis compañeros de trabajo brotan "Ooahhh dazso cuuuuuute"
Adel
66
@Adel: Casi me ahogo en mi desayuno, el teclado ahora está lleno o saliva y se
desmorona
2
@Adel, en cuanto al hablante no nativo, es bastante difícil para mí entender este humor, pero supongo que a tus compañeros de trabajo les gusta la pregunta :)
SiberianGuy
8
@Idsa: está transponiendo una respuesta de los compañeros de trabajo cuando se muestran los primeros pasos de un niño "Ooahhh dazso cuuuuuute" = "Oh, eso es tan lindo" (dicho con una voz de cantar-no-es-esa-muy-linda), con su respuesta al ver las pruebas unitarias escritas por Adel, al mirar los pasos de bebé de las pruebas unitarias, dicen "Oh, eso es tan lindo". Reacción a un - real - pasos de bebés = reacción a las pruebas unitarias "pasos de bebé".
Binario Worrier
3
@Binaryworrier desearía poder darte puntos reales por tomarte el tiempo para explicar la parentera
Andrew T Finnell

Respuestas:

31

Escriba el código más simple que hace pasar las pruebas.

Ninguno de ustedes hizo eso, por lo que puedo ver.

Bebé Paso 1.

Prueba: para la entrada "1,2" devuelve la suma de números que es 3

Haz que la prueba falle:

throw NotImplementedException();

Haz pasar la prueba:

return 3;

Bebé Paso 2.

Prueba: para la entrada "1,2" devuelve la suma de números, que es 3

Prueba: para la entrada "4,5" devuelve la suma de números, que es 9

La segunda prueba falla, así que hazla pasar:

numbers = input.Split(',');
return int.Parse(numbers[0]) + int.Parse(numbers[1]);

(Mucho más simple que una lista de if ... return)

Ciertamente puede argumentar la implementación obvia en este caso, pero si estaba hablando de hacerlo estrictamente en pasos pequeños, entonces estos son los pasos correctos, en mi opinión.

El argumento es que si no escribe la segunda prueba, luego podría aparecer una chispa brillante y "refactorizar" su código para leer:

return input.Length; # Still satisfies the first test

Y, sin tomar ambos pasos, nunca ha hecho que la segunda prueba se vuelva roja (lo que significa que la prueba en sí misma es sospechosa).

pdr
fuente
En cuanto a su ejemplo input.Length, con el mismo éxito que puedo imaginar alguna aplicación incorrecta loca que no será atrapado por las dos pruebas
SiberianGuy
@Idsa: Sí, absolutamente, y cuantas más pruebas escribas, más loca será la implementación. input.Lengthno es tan descabellado, especialmente si la entrada al método resulta ser una medida de algún archivo en algún lugar y, sin darse cuenta, llamó a su método Size().
pdr
66
+1. Con respecto a cómo aprender TDD, esta es la forma correcta. Una vez que lo haya aprendido, a veces puede ir directamente a la implementación obvia, pero para tener una idea de TDD, esto es mucho mejor.
Carl Manaster
1
Tengo una pregunta sobre la "prueba" en sí. ¿Escribiría una nueva prueba para la entrada "4,5", o modificaría la prueba original?
mxmissile
1
@mxmissile: escribiría una nueva prueba. No toma mucho tiempo y terminas con el doble de pruebas para protegerte cuando refactorizas más tarde.
pdr
50

Creo que la segunda forma es la mente entumecedoramente estúpida. Veo el valor de hacer pasos lo suficientemente pequeños, pero escribir esos pequeños pasos de cigoto (ni siquiera puedo llamarlos bebé) es una tontería y una pérdida de tiempo. Especialmente si el problema original que está resolviendo ya es muy pequeño por sí mismo.

Sé que es entrenamiento y se trata más de mostrar el principio, pero creo que tales ejemplos hacen que TDD sea más malo que bueno. Si desea mostrar el valor de los pequeños pasos, al menos use un problema donde haya algún valor.

Christophe Vanfleteren
fuente
+1 y gracias por hacerme mirar hacia arriba y aprender una nueva palabra (tonto)
Marjan Venema
12
+1 por llamarlo estúpidamente estúpido. TDD es muy bueno, pero al igual que con cualquier técnica de programación moderna, debes tener cuidado de no perderte.
stijn
2
"Especialmente si el problema original que estás resolviendo ya es muy pequeño en sí mismo". - Si la entrada fuera dos entradas para ser agregadas juntas, estaría de acuerdo con esto, pero no estoy convencido cuando es "dividir una cadena, analizar dos entradas del resultado y agregarlas". La mayoría de los métodos en el mundo real no son mucho más complicados que eso. De hecho, debería haber más pruebas por venir, para cubrir casos extremos como encontrar dos comas, valores no enteros, etc.
pdr
44
@pdr: Estoy de acuerdo con usted en que debería haber más pruebas para manejar los casos extremos. Cuando los escriba y note que su implementación necesita cambiar para manejarlos, haga todo lo que pueda. Supongo que solo tengo un problema con los pasos del cigoto para el primer camino feliz, "implementación obvia", en lugar de simplemente escribir eso e ir desde allí. No veo el valor en escribir una declaración if que cada fibra en mi cuerpo sabe que va a desaparecer al momento siguiente.
Christophe Vanfleteren
1
@ChristopheVanfleteren: cuando Beck describe una implementación obvia, usa la suma de dos entradas como ejemplo y todavía arroja una gran advertencia dramática sobre cómo morirás de vergüenza si tu pareja / revisor puede pensar en un código más simple que haga que el pase de prueba Eso es una certeza absoluta si solo escribe una prueba para este escenario. Además, puedo pensar en al menos tres formas "obvias" de resolver este problema: dividir y agregar, reemplazar la coma con + y evaluar, o usar expresiones regulares. El objetivo de TDD es llevarlo a la elección correcta.
pdr
19

Kent Beck cubre esto en su libro, Test Driven Development: By Example.

Su ejemplo indica una ' implementación obvia ': desea devolver la suma de dos valores de entrada, y este es un algoritmo bastante básico para lograr. Su contraejemplo cae en 'fingir hasta que lo logre' (aunque es un caso muy simple).

La implementación obvia puede ser mucho más complicada que esto, pero básicamente se activa cuando la especificación de un método es bastante estricta; por ejemplo, devuelve una versión codificada de URL de una propiedad de clase; no necesita perder el tiempo con un montón de codificaciones falsas

Una rutina de conexión de base de datos, por otro lado, necesitaría un poco más de reflexión y prueba para que no haya una implementación obvia (incluso si ya ha escrito varias veces en otros proyectos).

Del libro:

Cuando uso TDD en la práctica, comúnmente cambio entre estos dos modos de implementación, cuando todo va bien y sé qué escribir, pongo Implementación obvia después de la implementación obvia (ejecutando las pruebas cada vez para asegurarme de lo que es obvio para mí sigue siendo obvio para la computadora). Tan pronto como obtengo una barra roja inesperada, retrocedo, cambio a implementaciones falsas y refactorizo ​​al código correcto. Cuando mi confianza vuelve, vuelvo a Implementaciones obvias.

HorusKol
fuente
18

Veo esto como seguir la letra de la ley, pero no su espíritu.

Sus pequeños pasos deben ser:

Tan simple como sea posible, pero no más simple.

Además, el verbo en el método es sum

if (input == "1,2")
   return 3;

no es una suma, es una prueba para entradas específicas.

StuperUser
fuente
4

A mí me parece bien combinar varios pasos de implementación triviales en uno un poco menos trivial, lo hago todo el tiempo también. No creo que uno deba volverse religioso acerca de seguir TDD cada vez al pie de la letra.

OTOH esto aplica solo para pasos realmente triviales como el ejemplo anterior. Para cualquier cosa más compleja, que no pueda tener completamente en mi mente de una vez y / o donde no estoy 110% seguro sobre el resultado, prefiero ir paso a paso.

Péter Török
fuente
1

Cuando se establece por primera vez el camino de TDD, el tamaño de los pasos puede ser un problema confuso, como lo ilustra esta pregunta. Una pregunta que a menudo me hacía cuando comencé a escribir aplicaciones basadas en pruebas era; ¿La prueba que estoy escribiendo ayuda a impulsar el desarrollo de mis aplicaciones? Esto puede parecer trivial y no relacionado con algunos, pero aguanta conmigo por un momento.

Ahora, cuando me propongo escribir cualquier aplicación, generalmente comenzaré con una prueba. Cuánto de un paso de esa prueba se relaciona en gran medida con mi comprensión de lo que estoy tratando de hacer. Si creo que tengo el comportamiento de una clase en mi cabeza, entonces el paso será grande. Si el problema que estoy tratando de resolver es mucho menos claro, entonces el paso podría ser simplemente que sé que irá a un método llamado X y que devolverá Y. En este punto, el método ni siquiera tendrá ningún parámetro y existe la posibilidad de que el nombre del método y el tipo de retorno cambien. En ambos casos, las pruebas están impulsando mi desarrollo. Me están diciendo cosas sobre mi solicitud:

¿Esta clase que tengo en mi cabeza realmente va a funcionar?

o

¿Cómo demonios voy a hacer esto?

El punto es que puedo cambiar entre grandes pasos y pequeños pasos en un abrir y cerrar de ojos. Por ejemplo, si un gran paso no funciona y no puedo ver una forma obvia de evitarlo, cambiaré a un paso más pequeño. Si eso no funciona, cambiaré a un paso aún más pequeño. Luego hay otras técnicas como la triangulación si me quedo realmente atascado.

Si como yo eres un desarrollador y no un probador, entonces el objetivo de usar TDD no es escribir pruebas sino escribir código. No te obsesiones con escribir muchas pruebas pequeñas si no te dan ningún beneficio.

Espero que hayas disfrutado tu entrenamiento con TDD. En mi humilde opinión, si más personas se infectaran con la prueba, el mundo sería un lugar mejor :)

lexx
fuente
1

En un manual sobre las pruebas unitarias, leí el mismo enfoque (pasos que se ven muy, muy pequeños), y como respuesta a la pregunta "qué tan pequeños deberían ser", algo que me gustó, que fue (parafraseado) así:

Se trata de la confianza que tiene en que los pasos funcionan. Puedes dar grandes pasos si quieres. Pero, pruébalo por un tiempo y encontrarás mucha confianza equivocada en los lugares que lo das por sentado. Entonces, las pruebas lo ayudan a construir una confianza basada en hechos.

Entonces, tal vez tu colega sea un poco tímido :)

keppla
fuente
1

¿No es todo el punto que la implementación del método es irrelevante, siempre y cuando las pruebas tengan éxito? Ampliar las pruebas fallará más rápido en el segundo ejemplo, pero se puede hacer que falle en ambos casos.

Torácico
fuente
1
Es irrelevante si no te importa perder el tiempo
SiberianGuy
1

Estoy de acuerdo con la gente que dice que tampoco es la implementación más simple.

La razón por la cual la metodología es tan estricta es que te obliga a escribir tantas pruebas relevantes como sea posible. Devolver un valor constante para un caso de prueba y llamarlo aprobado está bien porque te obliga a regresar y especificar lo que realmente quieres para obtener algo más que tonterías de tu programa. Usar un caso tan trivial es dispararse en el pie en algunos aspectos, pero el principio es que los errores se infiltran en los vacíos en su especificación cuando intenta hacer 'demasiado' y reducir el requisito a la implementación más simple posible garantiza que La prueba debe escribirse para cada aspecto único de comportamiento que realmente desee.

Tom W
fuente
He agregado una actualización sobre "devolver un valor constante"
SiberianGuy