¿Cómo obtener la API inicial correcta usando TDD?

12

Esta podría ser una pregunta bastante tonta ya que estoy en mis primeros intentos de TDD. Me encantó la sensación de confianza que brinda y, en general, una mejor estructura de mi código, pero cuando comencé a aplicarlo en algo más grande que los ejemplos de juguetes de una clase, me encontré con dificultades.

Supongamos que estás escribiendo una especie de biblioteca. Sabe lo que tiene que hacer, conoce una forma general de cómo se supone que debe implementarse (en cuanto a la arquitectura), pero sigue "descubriendo" que necesita realizar cambios en su API pública a medida que codifica. Quizás necesite transformar este método privado en un patrón de estrategia (y ahora debe aprobar una estrategia simulada en sus pruebas), quizás haya perdido una responsabilidad aquí y allá y dividir una clase existente.

Cuando está mejorando el código existente, TDD parece encajar muy bien, pero cuando escribe todo desde cero, la API para la que escribe las pruebas es un poco "borrosa" a menos que haga un gran diseño por adelantado. ¿Qué haces cuando ya tienes 30 pruebas en el método que cambió su firma (y para esa parte, el comportamiento)? Esas son muchas pruebas para cambiar una vez que se suman.

Vytautas Mackonis
fuente
3
30 pruebas en un método? Parece que ese método tiene demasiada complejidad, o está escribiendo demasiadas pruebas.
Minthos
Bueno, puede que haya exagerado un poco para expresar mi punto. Después de verificar el código, generalmente tengo menos de 10 métodos por prueba, la mayoría de ellos por debajo de 5. Pero toda la parte de "volver y cambiarlos a mano" ha sido bastante frustrante.
Vytautas Mackonis
66
@Minthos: Puedo pensar en 6 pruebas en la parte superior de mi cabeza en las que cualquier método que tome una cadena a menudo fallará o funcionará mal en una escritura de primer borrador (nulo, vacío, demasiado largo, no localizado correctamente, escalado deficiente de rendimiento) . Del mismo modo para los métodos que toman una colección. Para un método no trivial, 30 suena grande, pero no demasiado poco realista.
Steven Evers

Respuestas:

13

Lo que llama "gran diseño por adelantado", lo llamo "planificación sensata de la arquitectura de su clase".

No se puede desarrollar una arquitectura a partir de pruebas unitarias. Incluso el tío Bob dice eso.

Si no está pensando en la arquitectura, si lo que está haciendo es ignorar la arquitectura y lanzar pruebas juntas y hacer que pasen, está destruyendo lo que permitirá que el edificio permanezca en pie porque es la concentración en el estructura del sistema y decisiones de diseño sólidas que ayudaron al sistema a mantener su integridad estructural.

http://s3.amazonaws.com/hanselminutes/hanselminutes_0171.pdf , página 4

Creo que sería más sensato abordar TDD desde una perspectiva de validación de su diseño estructural. ¿Cómo sabes que el diseño es incorrecto si no lo pruebas? ¿Y cómo verifica que sus cambios sean correctos sin cambiar también las pruebas originales?

El software es "suave" precisamente porque está sujeto a cambios. Si no se siente cómodo con la cantidad de cambios, continúe adquiriendo experiencia en diseño arquitectónico, y la cantidad de cambios que necesitará hacer en las arquitecturas de sus aplicaciones disminuirá con el tiempo.

Robert Harvey
fuente
La cuestión es que, incluso con una "planificación sensata", esperas que cambie mucho. Por lo general, dejo aproximadamente el 80% de mi arquitectura inicial intacta con algunos cambios en el medio. Ese 20% es lo que me molesta.
Vytautas Mackonis
2
Creo que esa es solo la naturaleza del desarrollo de software. No puede esperar tener toda la arquitectura correcta en el primer intento.
Robert Harvey
2
+1, y no es contrario a TDD. TDD comienza cuando comienza a escribir código, precisamente cuando termina el diseño. TDD puede ayudarlo a ver lo que se perdió en su diseño, permitiéndole refactorizar el diseño y la implementación, y continuar.
Steven Evers
2
De hecho, según Bob (y estoy totalmente de acuerdo con él), escribir código también es diseño. Definitivamente es necesario tener una arquitectura de alto nivel, pero el diseño no termina cuando escribe su código.
Michael Brown
Muy buena respuesta que da en el clavo. Veo a muchas personas, tanto a favor como en contra de TDD, que parecen tomar "no un gran diseño por adelantado" como "no diseñar en absoluto, solo codificar" cuando en realidad es un ataque contra las locas etapas de diseño de la cascada de antaño. El diseño siempre es una buena inversión de tiempo y es crucial para el éxito de cualquier proyecto no trivial.
Sara
3

Si haces TDD. No puede cambiar la firma y el comportamiento sin haberlo conducido por pruebas. Entonces, las 30 pruebas que fallaron fueron eliminadas en el proceso o cambiadas / refactorizadas junto con el código. O ahora están obsoletos, es seguro eliminarlos.

¿No puede ignorar el rojo 30 veces en su ciclo rojo-verde-refactorizador?

Sus pruebas deben ser refactorizadas junto con su código de producción. Si puede permitirse, vuelva a ejecutar todas las pruebas después de cada cambio.

No tenga miedo de eliminar las pruebas TDD. Algunas pruebas terminan probando bloques de construcción para llegar al resultado deseado. El resultado deseado a nivel funcional es lo que cuenta. Las pruebas en torno a los pasos intermedios en el algoritmo que eligió / inventó pueden ser de gran valor o no cuando hay más de una forma de alcanzar el resultado o si inicialmente se encontró con un callejón sin salida.

A veces puede crear algunas pruebas de integración decentes, conservarlas y eliminar el resto. De alguna manera depende de si trabajas de adentro hacia afuera o de arriba hacia abajo y de cuán grandes pasos das.

Joppe
fuente
1

Como acaba de decir Robert Harvey, probablemente esté intentando utilizar TDD para algo que debería ser manejado por una herramienta conceptual diferente (es decir, "diseño" o "modelado").

Intente diseñar (o "modelar") su sistema de una manera bastante abstracta ("general", "vaga"). Por ejemplo, si tiene que modelar un automóvil, solo tenga una clase de automóvil con algún método y campo vagos, como startEngine () y asientos int. Es decir: describa lo que quiere exponer al público , no cómo quiere implementarlo. Intente exponer solo las funcionalidades básicas (lectura, escritura, inicio, detención, etc.) y deje el código del cliente elaborado (prepareMyScene (), killTheEnemy (), etc.).

Escriba sus pruebas asumiendo esta sencilla interfaz pública.

Cambie el comportamiento interno de sus clases y métodos cuando lo necesite.

Si necesita cambiar su interfaz pública y su conjunto de pruebas, deténgase y piense. Lo más probable es que esto sea una señal de que hay algo mal en su API y en su diseño / modelado.

No es inusual cambiar una API. La mayoría de los sistemas en su versión 1.0 advierten explícitamente a los programadores / usuarios contra posibles cambios en su API. A pesar de esto, un flujo continuo e incontrolado de cambios en la API es una señal clara de un diseño incorrecto (o totalmente perdido).

Por cierto: por lo general, solo debe tener un puñado de pruebas por método. Un método, por definición, debe implementar una "acción" claramente definida en algún tipo de datos. En un mundo perfecto, esta debería ser una acción única que corresponde a una sola prueba. En el mundo real no es inusual (y no está mal) tener pocas "versiones" diferentes de la misma acción y pocas pruebas correspondientes diferentes. Por supuesto, debe evitar hacerse 30 pruebas con el mismo método. Esta es una señal clara de que el método intenta hacer demasiado (y su código interno se descontrola).

AlexBottoni
fuente
0

Lo miro desde la perspectiva del usuario. Por ejemplo, si sus API me permiten crear un objeto Person con nombre y edad, es mejor que haya un constructor Person (string string, int age) y métodos de acceso para name y age. Es simple crear casos de prueba para personas nuevas con y sin nombre y edad.

doug

SnoopDougieDoug
fuente