2009-08-25 24 views
5

Hace unas semanas comencé mi primer proyecto con TDD. Hasta ahora, solo he leído un libro sobre eso.Cómo desarrollar métodos complejos con TDD

Mi principal preocupación: cómo escribir pruebas para métodos/clases complejos. Escribí una clase que calcula una distribución binomial. Por lo tanto, un método de esta clase toma n, k, y p como entrada, y calcula el resp. probabilidad. (De hecho, hace un poco más, es por eso que tuve que escribirlo yo mismo, pero sigamos con esta descripción de la clase, para facilitar el argumento.)

Lo que hice para probar este método es: copiar algunos tablas con n diferentes que encontré en la web en mi código, escogiendo aleatoriamente una entrada en esta tabla, me pasé el resp. valores para n, k y p en mi función, y busqué si el resultado estaba cerca del valor en la tabla. Repito esto varias veces para cada mesa.

Todo esto funciona bien ahora, pero después de escribir la prueba, tuve que tanquear durante unas horas para realmente codificar la funcionalidad. Después de leer el libro, tuve la impresión de que no debería codificar más de unos minutos, hasta que la prueba muestre el color verde nuevamente. ¿Qué hice mal aquí? Por supuesto que he roto esta tarea en muchos métodos, pero todos son privados.

Una pregunta relacionada: ¿fue una mala idea elegir números al azar de la mesa? En caso de error, mostraré la semilla aleatoria utilizada por esta ejecución, para poder reproducir el error.

+0

"Tuve la impresión de que no debería programar más de unos minutos, hasta que la prueba vuelva a mostrarse en verde". ¿Dónde - específicamente - obtuvo esta impresión? Por favor, proporcione la cita o referencia. Esto rara vez es verdad, y tengo curiosidad por dónde lo leíste/lo viste/escuchaste. –

+0

Estaba en un libro alemán, "Testegetriebene Entwicklung mit JUnit & FIT", de Frank Westphal, 1ra Edición. P.ej. en la página 13, las dos primeras oraciones. – matthias

+1

Y como es muy probable que no tenga acceso al libro, intento una traducción: "La interacción entre el desarrollo impulsado por prueba y el diseño simple resulta en un ciclo de codificación minuto a minuto. De hecho, no codifica más tiempo que solo unos minutos, sin cerrar el ciclo de retroalimentación por medio de pruebas ". (Bueno, me estoy acercando a las limitaciones de mi inglés aquí, espero que esta traducción sea correcta.) – matthias

Respuesta

3

"Tuve la impresión de que no debería programar más de unos minutos, hasta que la prueba vuelva a mostrar el color verde. ¿Qué hice mal aquí?"

Westphal es correcto hasta cierto punto.

Algunas funciones son simples y se pueden probar simplemente y codificar de forma simple.

Algunas funciones no comienzan de forma simple. Simple es difícil de lograr. EWD dice que la simplicidad no se valora porque es muy difícil de lograr.

Si su cuerpo de función es difícil de escribir, no es simple. Esto significa que tienes que trabajar mucho más para reducirlo a algo simple.

Después de que finalmente logre la simplicidad, usted también puede escribir un libro que muestre lo simple que es.

Hasta que logre la simplicidad, llevará mucho tiempo escribir cosas.

"¿Fue una mala idea elegir números al azar de la mesa?"

Sí. Si tiene datos de muestra, ejecute su prueba en todos los datos de muestra. Use un bucle o algo así y pruebe todo lo que pueda probar.

No seleccione una fila: al azar o de lo contrario, seleccione todas las filas.

+0

Gracias por su comentario. Tengo la sensación de que tienes razón. Simplemente tengo que aprender mucho. Y mientras no tenga meses/años de experiencia, tengo que aceptar que mis soluciones no son óptimas. – matthias

+0

No es una cuestión de "óptimo". El problema es que "simple" es realmente, muy difícil de lograr. Muy pocos pueden hacer un buen trabajo para lograr la verdadera simplicidad. Los ejemplos de libros controvertidos son los peores, ya que el autor ha tenido mucho tiempo para llegar a lo simple. Todos tenemos que trabajar en eso; pueden ocultar la gran cantidad de esfuerzo requerido para llegar a un simple ejemplo. –

0

Es difícil responder a su pregunta sin saber un poco más sobre las cosas que quería implementar. Parece que no eran fácilmente divisibles en partes comprobables. O la funcionalidad funciona como un todo o no funciona. Si este es el caso, no es de extrañar que utilices herramientas para implementarlo.

En cuanto a su segunda pregunta: Sí, creo que es una mala idea hacer el accesorio de prueba al azar. ¿Por qué hiciste esto en primer lugar? Cambiar el dispositivo cambia la prueba.

1

Debe usar TDD con pasos de bebé. Intente pensar en pruebas que requerirán menos código para ser escritas. Luego escribe el código. Luego escribe otra prueba, y así sucesivamente.

Trate de resolver su problema en pequeños problemas (probablemente haya usado otros métodos para completar el código). Podrías TDD estos métodos más pequeños.

--edit - basado en los comentarios

métodos privados de análisis no es necesariamente una mala cosa. A veces, realmente contienen detalles de implementación, pero a veces también pueden actuar como una interfaz (en este caso, puedes seguir mi sugerencia en el siguiente párrafo).

Otra opción es crear otras clases (implementadas con las interfaces que se inyectan) para asumir algunas de las responsabilidades (tal vez algunos de esos métodos más pequeños), probarlas por separado y simularlas al probar la clase principal.

Finalmente, no veo pasar más tiempo codificando como un problema realmente grande. Algunos problemas son realmente más complejos de implementar que de probar y requieren mucho tiempo para pensar.

+0

Sí, podría TDD estos métodos más pequeños, pero luego tuve para probar el método privado s. Y tal como lo entendí, forman parte de los detalles de implementación, y no deberían probarse. – matthias

+0

acaba de editar mi respuesta –

+0

Si ayuda a hacer TDD de la forma que desee, puede comenzar con estos métodos privados siendo públicos/protegidos, y probar esas piezas más pequeñas de funcionalidad. Cuando haya creado los métodos públicos, que invocan a estos, puede eliminar las pruebas que prueban directamente los métodos privados. También debe preguntarse si tenerlos en visibilidad protegida/predeterminada, para probarlos desde el mismo paquete, va a dañar la encapsulación. Si no, diría que es una compensación justa. – Grundlefleck

0

Evite desarrollar métodos complejos con TDD hasta que haya desarrollado métodos simples como bloques de construcción para los métodos más complejos. TDD normalmente se usaría para crear una cantidad de funcionalidad simple que podría combinarse para producir un comportamiento más complejo. Los métodos/clases complejos siempre deben poder descomponerse en partes más simples, pero no siempre es obvio cómo y con frecuencia es un problema específico. La prueba que ha escrito parece que podría ser una prueba de integración más para asegurarse de que todos los componentes funcionen juntos correctamente, aunque la complejidad del problema que describe solo limita al requerir un conjunto de componentes para resolverlo. La situación que usted describe suena como esto:

class A { doLotsOfStuff pública() // Llama doTask1..n doTask1 privada() doTask2 privada() doTask3 privada() }

Se hará Encontrarlo es bastante difícil de desarrollar con TDD si comienzas escribiendo una prueba para la mayor unidad de funcionalidad (es decir, doLotsOfStuff()). Al dividir el problema en fragmentos más manejables y abordarlo desde el final de la funcionalidad más simple, también podrá crear pruebas más discretas (¡mucho más útiles que las pruebas que comprueban todo!). Tal vez su posible solución podría reformularse así:

class A { doLotsOfStuff pública() // Llama doTask1..n doTask1 pública() pública doTask2() doTask3 pública() }

Mientras que sus métodos privados pueden ser detalles de implementación que no son una razón para evitar probarlos de forma aislada. Al igual que muchos problemas, un enfoque de dividir y vencer sería afectivo aquí. La verdadera pregunta es, ¿qué tamaño es un trozo de funcionalidad adecuadamente comprobable y fácil de mantener? Solo usted puede responder eso en base a su conocimiento del problema y su propio juicio de aplicar sus habilidades a la tarea.

1

Tiene razón acerca de los refactores rápidos cortos, rara vez paso más de unos minutos entre la reconstrucción y la prueba, sin importar cuán complicado sea el cambio. Toma un poco de practica.

Sin embargo, la prueba que describió es más una prueba de sistema que una prueba unitaria. Una prueba unitaria intenta nunca probar más de un único método: para reducir la complejidad, probablemente deba dividir el problema en varios métodos.

Probablemente la prueba del sistema se debe realizar después de haber desarrollado su funcionalidad con pequeñas pruebas unitarias en métodos pequeños y directos.

Incluso si los métodos son simplemente tomando una parte de la fórmula de un método más largo, se obtiene la ventaja de facilidad de lectura (el nombre del método debe ser más legible que la parte fórmula que sustituye) y si los métodos son definitivas del JIT debería alinearlos para que no pierdas velocidad.

Por otro lado, si su fórmula no es tan grande, tal vez simplemente escriba todo en un método y lo pruebe como lo hizo y tome el tiempo de inactividad: las reglas están hechas para romperse.

6

No estoy de acuerdo con las personas que dicen que está bien probar el código privado, incluso si los convierte en clases separadas. Debe probar los puntos de entrada a su aplicación (o su biblioteca, si es una biblioteca que está codificando). Cuando prueba el código privado, limita sus posibilidades de factorización para más adelante (porque refactorizar sus clases privadas significa refactorizar su código de prueba, lo cual debe abstenerse de hacer). Si termina reutilizando este código privado en otra parte, entonces seguro, cree clases separadas y pruébelas, pero hasta que lo haga, suponga que No lo va a necesitar.

Para responder a su pregunta, creo que sí, en algunos casos, no es una situación de "2 minutos hasta que se vuelva verde". En esos casos, creo que está bien que las pruebas tarden mucho tiempo en volverse verdes. Pero la mayoría de las situaciones son "2 minutos hasta que se ponga verde". En tu caso (no sé squat sobre distribución binomial), escribiste que tienes 3 argumentos, n, k y p. Si mantiene k y p constantes, ¿es más fácil de implementar su función? En caso afirmativo, debe comenzar creando pruebas que siempre tengan constante k y p. Cuando pasen tus pruebas, introduce un nuevo valor para k, y luego para p.

0

Creo que el estilo de prueba que tiene es totalmente apropiado para el código, que es principalmente un cálculo. En lugar de elegir una fila aleatoria de la tabla de resultados conocidos, sería mejor codificar los casos extremos significativos. De esta manera, tus pruebas verifican constantemente lo mismo, y cuando uno se rompe, sabes lo que era.

Sí TDD establece períodos cortos desde la prueba hasta la implementación, pero lo que ha bajado aún supera los estándares que encontrará en la industria. Ahora puede confiar en el código para calcular cómo debería hacerlo, y puede refactorizar/extender el código con un grado de certeza de que no lo está rompiendo.

A medida que aprende más técnicas de prueba, puede encontrar un enfoque diferente que acorte el ciclo rojo/verde. Mientras tanto, no te sientas mal por eso. Es un medio para un fin, no un fin en sí mismo.