2010-07-16 12 views
9

Estoy jugando con un proyecto de juguete en casa para comprender mejor el diseño impulsado por prueba. Al principio las cosas parecían estar yendo bien y me metí al ritmo de las pruebas fallidas, el código, la prueba de aprobación.Test Driven Design: ¿dónde me equivoqué?

Luego vine a agregar una prueba y me di cuenta de que sería difícil con mi estructura actual y que además debería dividir una clase en particular que tenía demasiadas responsabilidades. Agregar aún más responsabilidades para la próxima prueba fue claramente erróneo. Decidí dejar de lado esta prueba y refactorizar lo que tenía. Aquí es donde las cosas comenzaron a ir mal.

Era difícil refactorizar sin hacer muchas pruebas a la vez, y entonces la única opción parecía ser hacer muchos cambios y esperar terminar en algo donde las pruebas pasaran nuevamente. Las pruebas en sí eran válidas, solo tuve que romper casi todas mientras refactorizaba. La refactorización (que todavía no me gusta) me llevó cinco o seis horas antes de volver a todas las pruebas. Las pruebas me ayudaron en el camino.

Parece que salí de la pista TDD. ¿Qué crees que hice mal?

Como esto es principalmente un ejercicio de aprendizaje, estoy considerando deshacer toda esa refactorización e intentar avanzar de nuevo de una mejor manera.

+3

Me gusta el compromiso con el aprendizaje. Creo que retroceder y aplicar la respuesta de @ phillipe es una gran idea. – btlog

Respuesta

8

Quizás fuiste demasiado rápido al dividir tu clase. Los pasos para la Extract Class Refactoring son los siguientes:

  • crear la nueva clase
  • tienen una instancia de esa clase como un miembro de datos privados
  • move field a la nueva clase, uno por uno
  • de compilación y prueba para cada campo
  • move method a la nueva clase, una por una, las pruebas para cada

De esa manera no se romperán una gran cantidad de pruebas mientras se refactoriza su clase, y puede confiar en las pruebas para asegurarse de que nada se haya roto hasta ahora a lo largo de la división de clases.

Además, asegúrese de que sea probando el comportamiento, no la implementación.

+1

+1. También puede llevar a cabo este proceso de forma controlada por pruebas. Dejando de lado Move Field por el momento: cuando esté listo para Mover el Método M a la nueva clase, primero redirija su (s) prueba (s) unitaria (es) para M a esa nueva clase, posiblemente también moviendo la prueba a una clase diferente. Cuando M se mueve, su (s) prueba (s) debe (n) compilarse y aprobarse. –

+0

Gracias por esta respuesta, puedo revivir el código y probar esto. Es difícil de explicar sin publicar mucho código, pero las responsabilidades que trato de separar son "lo que el usuario quiere que suceda" y "lo que está permitido que suceda". Estos conceptos se mezclaron en la clase que me di cuenta. –

+0

No olvides usar la inyección de dependencia para la clase extraída en la clase anterior – Gutzofter

0

Quizás estaba probando un nivel demasiado bajo. Es difícil de decir sin ver tu código, pero normalmente pruebo una función de extremo a extremo y me aseguro de que todo el comportamiento que esperaba que sucediera sucedió. Probar cada método de manera aislada te dará la web de prueba que has creado.

Puede utilizar herramientas como NCover y DotCover para comprobar que no ha omitido ninguna ruta de código.

+0

Mis pruebas fueron en general: construir una parte del modelo de dominio, darle un evento o dos desde la interfaz de usuario (simulada) y afirmar cuál es el resultado al consultar el modelo de dominio. Mis pruebas usaban la misma interfaz que la capa UI porque la construí de arriba hacia abajo. (Algunas otras pruebas fueron solo pruebas unitarias en una clase en particular para entender bien). –

+1

Unit Testing está probando cada método de manera aislada. TDD está creando pruebas unitarias antes de codificar la implementación. – btlog

+0

Las pruebas que llamé "pruebas unitarias" todavía se escribieron primero. Simplemente estaban enfocados en una sola clase. Los otros tenían un alcance mayor (comenzando con un evento GUI simulado y hasta un resultado simulado de la GUI). –

0

Lo único "incorrecto" fue agregar una prueba después. En TTD "verdadero" primero declaras todas las pruebas antes de la implementación real. Digo "verdadero" porque a menudo eso es solo teoría. Pero en la práctica todavía tienes la seguridad dada por las pruebas.

+0

Creo que quiso decir que la prueba que agregó después de hacer algún trabajo fue para una nueva característica, no para el código existente. –

+0

Iba a agregar una prueba para la próxima pieza de funcionalidad cuando me di cuenta de que no encajaría sin hinchar aún más una clase existente. Fue entonces cuando decidí que necesitaba refactorizar antes de intentar agregar más funciones (actualicé la pregunta para aclarar). –

+0

Ah bien ... sí, diseñar un sistema de forma que sea extensible para funciones imprevistas es siempre un gran desafío. Supongo que esa es una de esas cosas en donde leer libros no te llevará demasiado lejos y (principalmente) todo se reduce a la experiencia. – Mene

0

Esto es, lamentablemente, algo que los defensores de TDD no hablan lo suficiente y eso hace que la gente intente con TDD y luego lo abandone.

Lo que hago es lo que llamo "Pruebas de alto nivel", que consiste en evitar las pruebas unitarias y realizar exclusivamente pruebas de alto nivel (lo que podríamos llamar "pruebas de integración"). Funciona bastante bien y evito el problema (muy importante) que mencionaste.Escribí un artículo sobre él hace un tiempo:

http://www.hardcoded.net/articles/high-level-testing.htm

Buena suerte con TDD, no renunciar todavía.

+1

-1, porque las pruebas unitarias son muy importantes, muy valiosas. Las pruebas de alto nivel te dicen que algo salió mal, y eso es muy valioso también, pero las pruebas unitarias te dicen qué ha ido mal. No hagas sin ellos. –

+1

Pero lo que sucede es que las pruebas unitarias se vuelven un inconveniente porque hacen que la refactorización lleve más tiempo. Cuando eso sucede, se vuelve * menos propenso * a la refactorización. Eso es malo. –

+1

La prueba de unidad solo se convierte en una responsabilidad si se les permite. La refactorización en realidad es algo que hago con mucha más confianza con muchas pruebas unitarias. Sí, es posible que tenga pruebas de ruptura por un tiempo, pero esto se puede evitar de la manera que describe Phillippe, o es algo que se da por sentado mientras se trabaja en la refactorización y se vuelven a ver las pruebas una por una, y algunas veces muchos por muchos –

2

Quería comentar la respuesta aceptada, pero mi reputación actual no me permite. Entonces aquí está como una nueva respuesta.

TDD dice:

Crear una prueba que falla. Codifica un poco. Haga pasar la prueba.

Insiste en la codificación en pequeños pasos (especialmente al comenzar). Vea TDD como una validación sistemática de las refactorizaciones sucesivas que realiza para construir sus programas. Si da un paso demasiado grande, su refactorización se saldrá de control.

+0

Pero estaba en una posición en la que pasaban todas mis pruebas (excepto la última que acababa de escribir), pero no estaba contento con la estructura del código, ya que no me permitía implementar fácilmente la siguiente prueba. Después de esto, la refactorización que realicé salió mal. –

0

También para TDD, la regresión continua de casos de prueba también es necesaria. Entonces la integración continua con las herramientas de Cobertura (como se mencionó anteriormente) es necesaria. De modo que los pequeños cambios (es decir, refactorización) pueden modificarse fácilmente y se puede encontrar fácilmente cualquier ruta de código que se haya perdido.

También creo que si las pruebas no se escribieron previamente, no debe perderse el tiempo para pensar si escribir pruebas o no. Las pruebas deben escribirse de inmediato.