¿Cómo manejan las personas que realizan TDD la pérdida de trabajo cuando realizan una refactorización importante?

37

Durante un tiempo he estado tratando de aprender a escribir pruebas unitarias para mi código.

Inicialmente comencé a hacer TDD verdadero, donde no escribiría ningún código hasta que escribiera una prueba fallida primero.

Sin embargo, recientemente tuve que resolver un problema espinoso que involucraba mucho código. Después de pasar un par de semanas escribiendo pruebas y luego código, llegué a la desafortunada conclusión de que todo mi enfoque no iba a funcionar, y que tendría que tirar dos semanas de trabajo y comenzar de nuevo.

Esta es una decisión bastante mala cuando acabas de escribir el código, pero cuando también has escrito cientos de pruebas unitarias, se vuelve aún más emocionalmente difícil tirarlo todo a la basura.

No puedo evitar pensar que he desperdiciado 3 o 4 días de esfuerzo escribiendo esas pruebas cuando podría haber reunido el código como prueba de concepto y luego haber escrito las pruebas una vez que estuve satisfecho con mi enfoque.

¿Cómo manejan adecuadamente esas situaciones las personas que practican TDD? ¿Hay algún caso para doblar las reglas en algunos casos o siempre escribes servilmente las pruebas primero, incluso cuando ese código puede resultar inútil?

GazTheDestroyer
fuente
66
La perfección se logra, no cuando no hay nada más que agregar, sino cuando no hay nada más que quitar. - Antoine de Saint-Exupery
mouviciel
12
¿Cómo es posible que todas tus pruebas estén mal? Explique cómo un cambio en la implementación invalida cada prueba que escribió.
S.Lott
66
@ S.Lott: Las pruebas no estaban mal, simplemente ya no eran relevantes. Digamos que está resolviendo parte de un problema usando números primos, por lo que escribe una clase para generar números primos y escribe pruebas para esa clase para asegurarse de que esté funcionando. Ahora encuentra otra solución totalmente diferente a su problema que no involucra primos de ninguna manera. Esa clase y sus pruebas ahora son redundantes. Esta era mi situación solo con 10 de clases, no solo una.
GazTheDestroyer
55
@GazTheDestroyer me parece que distinguir entre el código de prueba y el código funcional es un error, todo es parte del mismo proceso de desarrollo. Es justo notar que TDD tiene una sobrecarga que generalmente se recupera más adelante en el proceso de desarrollo y que parece que esa sobrecarga no le ha ganado nada en este caso. Pero igualmente, ¿cuánto informaron las pruebas a su comprensión de las fallas de la arquitectura? También es importante tener en cuenta que se le permite (más aún, alentarlo ) a podar sus pruebas con el tiempo ... aunque esto es probablemente un poco extremo (-:
Murph
10
Voy a ser semánticamente pedante y a estar de acuerdo con @ S.Lott aquí; lo que hiciste no es refactorizar si resulta en tirar muchas clases y las pruebas para ellas. Eso es rediseñar . La refactorización, especialmente en el sentido TDD, significa que las pruebas fueron verdes, cambió un código interno, volvió a ejecutar las pruebas y se mantuvieron verdes.
Eric King

Respuestas:

33

Siento que hay dos problemas aquí. La primera es que no se dio cuenta de antemano de que su diseño original puede no ser el mejor enfoque. Si lo hubiera sabido de antemano, es posible que haya optado por desarrollar uno o dos prototipos de descarte rápido , para explorar las posibles opciones de diseño y evaluar cuál es la forma más prometedora de seguir. En la creación de prototipos, no necesita escribir código de calidad de producción y no necesita probar la unidad en cada rincón y grieta (o en absoluto), ya que su único objetivo es aprender, no pulir el código.

Ahora, darse cuenta de que necesita prototipos y experimentos en lugar de comenzar a desarrollar el código de producción de inmediato, no siempre es fácil y ni siquiera siempre es posible. Armado con el conocimiento recién adquirido, es posible que pueda reconocer la necesidad de crear prototipos la próxima vez. O tal vez no. Pero al menos ahora sabe que esta opción debe considerarse. Y esto en sí mismo es un conocimiento importante.

El otro problema es en mi humilde opinión con su percepción. Todos cometemos errores, y es muy fácil ver en retrospectiva lo que deberíamos haber hecho de manera diferente. Así es como aprendemos. Anote su inversión en pruebas unitarias como el precio de aprender que la creación de prototipos puede ser importante y supérela. Solo esfuérzate por no cometer el mismo error dos veces :-)

Péter Török
fuente
2
Sabía que iba a ser un problema difícil de resolver y que mi código sería algo exploratorio, pero estaba entusiasmado con mis recientes éxitos de TDD, así que seguí escribiendo pruebas como lo había hecho, ya que eso es todo. La literatura de TDD hace mucho hincapié. Así que sí, ahora sé que las reglas pueden romperse (de eso se trataba mi pregunta realmente). Probablemente lo atribuya a la experiencia.
GazTheDestroyer
3
"Seguí escribiendo pruebas como lo había hecho, ya que eso es lo que toda la literatura de TDD enfatiza tanto". Probablemente debería actualizar la pregunta con la fuente de su idea de que todas las pruebas deben escribirse antes de cualquier código.
S.Lott
1
No tengo esa idea y no estoy seguro de cómo lo sacaste del comentario.
GazTheDestroyer
1
Iba a escribir una respuesta, pero en su lugar voté por la suya. Sí, un millón de veces sí: si todavía no sabe cómo se ve su arquitectura, escriba primero un prototipo desechable y no se moleste en escribir pruebas unitarias durante la creación de prototipos.
Robert Harvey
1
@WarrenP, seguramente hay personas que piensan que TDD es la única forma verdadera (cualquier cosa puede convertirse en una religión si te esfuerzas lo suficiente ;-). Aunque prefiero ser pragmático. Para mí, TDD es una herramienta en mi caja de herramientas, y la uso solo cuando ayuda, en lugar de obstaculizar, la resolución de problemas.
Péter Török
8

El punto de TDD es que te obliga a escribir pequeños incrementos de código en funciones pequeñas , precisamente para evitar este problema. Si ha pasado semanas escribiendo código en un dominio, y cada método de utilidad que escribió se vuelve inútil cuando reconsidera la arquitectura, entonces sus métodos son ciertamente demasiado grandes en primer lugar. (Sí, soy consciente de que esto no es exactamente reconfortante ahora ...)

Kilian Foth
fuente
3
Mis métodos no eran grandes en absoluto, simplemente se volvieron irrelevantes dada la nueva arquitectura que no se parecía en nada a la arquitectura anterior. En parte porque la nueva arquitectura era mucho más simple.
GazTheDestroyer
Muy bien, si realmente no hay nada reutilizable, solo puedes reducir tus pérdidas y seguir adelante. Pero la promesa de TDD es que te hace alcanzar los mismos objetivos más rápido, a pesar de que escribes un código de prueba además del código de la aplicación. Si eso es cierto, y creo firmemente que lo es, entonces al menos llegaste al punto en que te diste cuenta de cómo hacer la arquitectura en "un par de semanas" en lugar del doble de ese tiempo.
Kilian Foth
1
@Kilian, re "la promesa de TDD es que te hace alcanzar las mismas metas más rápido" - ¿a qué metas te refieres aquí? Es bastante obvio que escribir pruebas unitarias junto con el código de producción en sí lo hace más lento inicialmente , en comparación con solo producir código. Diría que TDD solo va a pagar a largo plazo, debido a la mejora de la calidad y la reducción de los costos de mantenimiento.
Péter Török
@ PéterTörök: hay personas que insisten en que TDD nunca tiene ningún costo porque se paga por sí mismo cuando se escribe el código. Ciertamente, ese no es el caso para mí, pero Killian parece creerlo por sí mismo.
psr
Bueno ... si no crees eso, de hecho, si no crees que TDD tiene una recompensa sustancial en lugar de un costo, entonces no hay ningún punto en hacerlo, ¿verdad? No solo en la situación muy específica que describió Gaz, sino en absoluto . Me temo que ahora he llevado este hilo completamente fuera de tema :(
Kilian Foth
6

Brooks dijo "planea tirar uno; lo harás, de todos modos". Me parece que estás haciendo exactamente eso. Dicho esto, debe escribir sus pruebas unitarias para probar la unidad de código y no una gran franja de código. Esas son pruebas más funcionales y, por lo tanto, deberían sobre cualquier implementación interna.

Por ejemplo, si quiero escribir un solucionador de PDE (ecuaciones diferenciales parciales), escribiría algunas pruebas tratando de resolver cosas que puedo resolver matemáticamente. Esas son mis primeras pruebas de "unidad" - lea: las pruebas funcionales se ejecutan como parte de un marco xUnit. Esos no cambiarán según el algoritmo que use para resolver el PDE. Todo lo que me importa es el resultado. Las pruebas de la segunda unidad se centrarán en las funciones utilizadas para codificar el algoritmo y, por lo tanto, serían específicas del algoritmo, por ejemplo, Runge-Kutta. Si descubriera que Runge-Kutta no era adecuado, aún tendría esas pruebas de nivel superior (incluidas las que mostraron que Runge-Kutta no era adecuado). Por lo tanto, la segunda iteración todavía tendría muchas de las mismas pruebas que la primera.

Su problema puede ser el diseño y no necesariamente el código. Pero sin más detalles, es difícil de decir.

Sardathrion - Restablece a Monica
fuente
Es solo periférico, pero ¿qué es PDE?
un CVn
1
@ MichaelKjörling Supongo que es una ecuación diferencial parcial
miedo
2
¿Brooks no se retractó de esa declaración en su segunda edición?
Simon
¿Cómo quiere decir que todavía tendrá las pruebas que muestran que Runge-Kutta no era adecuado? ¿Cómo son esas pruebas? ¿Quiere decir que guardó el algoritmo Runge-Kutta que escribió, antes de descubrir que no era adecuado, y que fallarían las pruebas de extremo a extremo con RK en la mezcla?
moteutsch
5

Debe tener en cuenta que TDD es un proceso iterativo. Escriba una pequeña prueba (en la mayoría de los casos, unas pocas líneas deberían ser suficientes) y ejecútela. La prueba debería fallar, ahora trabaje directamente en su fuente principal e intente implementar la funcionalidad probada para que la prueba pase. Ahora comienza de nuevo.

No debe intentar escribir todas las pruebas de una vez, porque, como ha notado, esto no va a funcionar. Esto reduce el riesgo de perder el tiempo escribiendo pruebas que no se van a utilizar.

BenR
fuente
1
No creo que pueda haberme explicado muy bien. Escribo pruebas de forma iterativa. Así es como terminé con varios cientos de pruebas de código que de repente se volvieron redundantes.
GazTheDestroyer
1
Como se mencionó anteriormente, creo que debería considerarse como "pruebas y código" en lugar de "pruebas de código"
Murph
1
+1: "No debes intentar escribir todas las pruebas de una vez"
S.Lott
4

Creo que lo dijo usted mismo: no estaba seguro acerca de su enfoque antes de comenzar a escribir todas sus pruebas unitarias.

Lo que aprendí al comparar los proyectos TDD de la vida real con los que trabajé (no muchos, de hecho, solo 3 cubriendo 2 años de trabajo) con lo que aprendí teóricamente, es que las Pruebas automatizadas = Pruebas unitarias (sin, por supuesto, ser mutuamente exclusivo).

En otras palabras, la T en TDD no tiene que tener una U con ella ... Está automatizada, pero es menos una prueba unitaria (como en las clases y métodos de prueba) que una prueba funcional automatizada: está en el mismo nivel de granularidad funcional como la arquitectura en la que está trabajando actualmente. Comienzas a alto nivel, con pocas pruebas y solo el panorama funcional, y solo eventualmente terminas con miles de UT, y todas tus clases bien definidas en una hermosa arquitectura ...

Las pruebas unitarias le brindan una gran ayuda cuando trabaja en equipo, para evitar cambios en el código que crean ciclos interminables de errores. Pero nunca escribí algo tan preciso cuando comencé a trabajar en un proyecto, antes de tener al menos un POC de trabajo global para cada historia de usuario.

Tal vez es solo mi forma personal de hacer esto. No tengo la experiencia suficiente para decidir desde cero qué patrones o estructura tendrá mi proyecto, por lo que no perderé mi tiempo escribiendo cientos de UT desde el principio ...

En términos más generales, la idea de romper todo y tirarlo todo siempre estará ahí. Tan "continuo" como podemos tratar de ser con nuestras herramientas y métodos, a veces la única forma de combatir la entropía es comenzar de nuevo. Pero el objetivo es que cuando eso suceda, las pruebas automatizadas y unitarias que implementó harán que su proyecto ya sea menos costoso que si no existiera, y lo hará, si encuentra el equilibrio.

GFK
fuente
3
bien dicho - es TDD, no UTDD
Steven A. Lowe
Excelente respuesta En mi experiencia con TDD, es importante que las pruebas escritas se centren en los comportamientos funcionales del software y no en las pruebas unitarias. Es más difícil pensar en los comportamientos que necesita de una clase, pero conduce a interfaces limpias y potencialmente simplifica la implementación resultante (no agrega funcionalidad que realmente no necesita).
JohnTESlade
4
¿Cómo manejan adecuadamente esas situaciones las personas que practican TDD?
  1. al considerar cuándo crear un prototipo frente a cuándo codificar
  2. al darse cuenta de que las pruebas unitarias no son lo mismo que TDD
  3. mediante pruebas TDD escritas para verificar una característica / historia, no una unidad funcional

La combinación de las pruebas unitarias con el desarrollo basado en pruebas es fuente de mucha angustia y aflicción. Así que repasemos una vez más:

  • las pruebas unitarias se refieren a verificar cada módulo y función individuales en la implementación ; en UT verás un énfasis en cosas como métricas de cobertura de código y pruebas que se ejecutan muy rápidamente
  • el desarrollo basado en pruebas se ocupa de verificar cada característica / historia en los requisitos ; en TDD, verá un énfasis en cosas como escribir primero la prueba, asegurarse de que el código escrito no exceda el alcance previsto y refactorizar por calidad

En resumen: las pruebas unitarias tienen un enfoque de implementación, TDD tiene un enfoque de requisitos. No són la misma cosa.

Steven A. Lowe
fuente
"TDD tiene un enfoque en los requisitos" Estoy totalmente en desacuerdo con eso. Las pruebas que escribe en TDD son pruebas unitarias. Ellos hacen verificar cada función / método. TDD hace hincapié en la cobertura del código y se preocupa por las pruebas que se ejecutan rápidamente (y será mejor que lo hagan, ya que ejecuta las pruebas cada 30 segundos más o menos). Tal vez estabas pensando ATDD o BDD?
guillaume31
1
@ ian31: ejemplo perfecto de la combinación UT y TDD. Debe estar en desacuerdo y remitirlo a algún material de origen en.wikipedia.org/wiki/Test-driven_development : el propósito de las pruebas es definir los requisitos del código . BDD es genial. Nunca he oído hablar de ATDD, pero de un vistazo parece que aplico la escala TDD .
Steven A. Lowe
Puede utilizar perfectamente TDD para diseñar código técnico que no esté directamente relacionado con un requisito o una historia de usuario. Encontrará innumerables ejemplos de eso en la web, en libros, conferencias, incluidas las personas que iniciaron TDD y lo popularizaron. TDD es una disciplina, una técnica para escribir código, no dejará de ser TDD dependiendo del contexto en el que lo uses.
guillaume31
Además, del artículo de Wikipedia que mencionó: "Las prácticas avanzadas de desarrollo basado en pruebas pueden conducir a ATDD, donde los criterios especificados por el cliente se automatizan en pruebas de aceptación, que luego impulsan el proceso tradicional de desarrollo basado en pruebas unitarias (UTDD)". ...] Con ATDD, el equipo de desarrollo ahora tiene un objetivo específico para satisfacer, las pruebas de aceptación, lo que los mantiene continuamente enfocados en lo que el cliente realmente quiere de esa historia de usuario ". Lo que parece implicar que ATDD se centra principalmente en los requisitos, no en TDD (o UTDD como lo dicen).
guillaume31
@ ian31: La pregunta del OP sobre 'tirar varios cientos de pruebas unitarias' indicó una confusión de escala. Puede usar TDD para construir un cobertizo si lo desea. : D
Steven A. Lowe
3

El desarrollo basado en pruebas está destinado a impulsar su desarrollo. Las pruebas que escribes te ayudan a afirmar la exactitud del código que estás escribiendo actualmente y a aumentar la velocidad de desarrollo desde la primera línea en adelante.

Parece creer que las pruebas son una carga y solo están destinadas a un desarrollo incremental más adelante. Esta línea de pensamiento no está en línea con TDD.

Tal vez pueda compararlo con la escritura estática: aunque se puede escribir código sin información de tipo estático, agregar el tipo estático al código ayuda a afirmar ciertas propiedades del código, liberando la mente y permitiendo enfocarse en una estructura importante, aumentando así la velocidad y eficacia.

Dibbeke
fuente
2

El problema de realizar una refactorización importante es que puedes y a veces seguirás un camino que te llevará a darte cuenta de que has mordido más de lo que puedes masticar. Las refactorizaciones gigantes son un error. Si el diseño del sistema es defectuoso en primer lugar, la refactorización solo puede llevarlo lejos antes de que tenga que tomar una decisión difícil. O deje el sistema como está y trabaje alrededor de él, o planee rediseñar y hacer algunos cambios importantes.

Sin embargo, hay otra manera. El beneficio real de refactorizar el código es hacer las cosas más simples, fáciles de leer e incluso más fáciles de mantener. Cuando se acerca a un problema sobre el que tiene incertidumbre, marca un cambio, va tan lejos para ver a dónde podría conducir para obtener más información sobre el problema, luego tira el pico y aplica una nueva refactorización en función de lo que es el pico. te enseñó. La cuestión es que en realidad solo puede mejorar su código con certeza si los pasos son pequeños y sus esfuerzos de refactorización no superan su capacidad de escribir sus pruebas primero. La tentación es escribir una prueba, luego codificar, luego codificar un poco más porque una solución puede parecer obvia, pero pronto se da cuenta de que su cambio cambiará muchas más pruebas, por lo que debe tener cuidado de cambiar solo una cosa a la vez.

La respuesta, por lo tanto, es nunca hacer que su refactorización sea importante. Pequeños pasos. Comience por extraer métodos, luego busque eliminar la duplicación. Luego pase a extraer clases. Cada uno en pequeños pasos, un cambio menor a la vez. SI está extrayendo código, primero escriba una prueba. Si está eliminando el código, elimínelo y ejecute sus pruebas, y decida si alguna de las pruebas rotas será necesaria. Un pequeño paso de bebé a la vez. Parece que llevará más tiempo, pero en realidad acortará considerablemente el tiempo de refactorización.

Sin embargo, la realidad es que cada pico es aparentemente una pérdida potencial de esfuerzo. Los cambios de código a veces no van a ninguna parte, y te encuentras restaurando tu código desde tus vcs. Esto es solo una realidad de lo que hacemos día a día. Sin embargo, cada punta que falla no se desperdicia si te enseña algo. Cada esfuerzo de refactorización que falla le enseñará que está tratando de hacer demasiado y demasiado rápido, o que su enfoque puede estar equivocado. Eso tampoco es una pérdida de tiempo si aprendes algo de él. Cuanto más hagas esto, más aprenderás y más eficiente serás en ello. Mi consejo es que solo lo use por ahora, aprenda a hacer más haciendo menos y acepte que así es como probablemente deben ser las cosas hasta que mejore para identificar qué tan lejos tomar un pico antes de que no lo lleve a ninguna parte.

S.Robins
fuente
1

No estoy seguro de la razón por la cual su enfoque resultó defectuoso después de 3 días. Dependiendo de sus incertidumbres en su arquitectura, podría considerar cambiar su estrategia de prueba:

  • Si no está seguro sobre el rendimiento, ¿es posible que desee comenzar con algunas pruebas de integración que afirman el rendimiento?

  • Cuando la complejidad de la API es lo que está investigando, escriba algunas pruebas de unidades pequeñas y reales para descubrir cuál sería la mejor manera de hacerlo. No se moleste en implementar nada, solo haga que sus clases devuelvan valores codificados o haga que arrojen NotImplementedExceptions.

Boris Callens
fuente
0

Para mí, las pruebas unitarias también son una ocasión para poner la interfaz en uso "real" (bueno, ¡tan real como las pruebas unitarias!).

Si me veo obligado a configurar una prueba, tengo que ejercitar mi diseño. Esto ayuda a mantener las cosas cuerdas (si algo es tan complejo que escribir una prueba es una carga, ¿cómo será usarlo?).

Esto no evita cambios en el diseño, sino que expone la necesidad de ellos. Sí, una reescritura completa es un dolor. Para (intentar) evitarlo, generalmente configuro (uno o más) prototipos, posiblemente en Python (con el desarrollo final en c ++).

De acuerdo, no siempre tienes tiempo para todas estas golosinas. Esos son precisamente los casos en que se necesita un GRANDE cantidad de tiempo para lograr sus objetivos ... y / o para mantener todo bajo control.

Francesco
fuente
0

Bienvenido a desarrolladores creativos de circo .


En lugar de respetar todas las formas 'legales / razonables' de codificar al principio,
intente la intuición , sobre todo si es importante y nuevo para usted y si no hay una muestra que parezca que desea:

- Escriba con su instinto, a partir de cosas que ya sabe , no con tu mentalidad e imaginación.
- Y pare.
- Tome una lupa e inspeccione todas las palabras que escribe: escribe "texto" porque "texto" está cerca de String, pero se necesita "verbo", "adjetivo" o algo más preciso, lea nuevamente y ajuste el método con un nuevo sentido
. .. o, escribiste un código pensando en el futuro? quítelo
: corrija, realice otra tarea (deporte, cultura u otras cosas fuera de los negocios), vuelva y lea nuevamente.
- Todo encaja bien,
- Correcto, hacer otra tarea, volver y leer de nuevo.
- Todo encaja bien, pase a TDD
- Ahora todo está correcto, bien
- Intente el punto de referencia para señalar las cosas que deben optimizarse, hágalo.

Lo que parece:
escribiste un código que respeta todas las reglas
, obtienes una experiencia, una nueva forma de trabajar,
algo cambia en tu mente, nunca tendrás miedo con una nueva configuración.

Y ahora, si ve un UML similar al anterior, podrá decir
"Jefe, empiezo por TDD para esto ..." ¿
es otra cosa nueva?
"Jefe, probaría algo antes de decidir la forma en que codificaré ..."

Saludos desde PARIS
Claude

cl-r
fuente