test example ejemplo driven development book python tdd testdrivendesign

python - example - TDD-problemas de principiantes y escollos



test driven development c# (7)

Aunque he escrito pruebas unitarias para la mayoría de los códigos que he hecho, solo hace poco conseguí una copia de TDD con el ejemplo de Kent Beck. Siempre me he arrepentido de ciertas decisiones de diseño que tomé, ya que impidieron que la aplicación fuera "verificable". Leí el libro y, aunque algunos de ellos parecen extraños, sentí que podía manejarlo y decidí probarlo en mi proyecto actual, que es básicamente un sistema cliente / servidor donde las dos piezas se comunican a través. USB. Uno en el gadget y el otro en el host. La aplicación está en Python.

Comencé y muy pronto me enredé en un lío de reescrituras y minúsculas pruebas que luego supuse que en realidad no probaban nada. Deseché la mayoría de ellos y ahora tengo una aplicación para la cual las pruebas se han coagulado en solo 2.

Basado en mis experiencias, tengo algunas preguntas que me gustaría hacer. Obtuve algo de información de New to TDD: ¿Hay aplicaciones de muestra con pruebas para mostrar cómo hacer TDD? pero tengo algunas preguntas específicas sobre las que me gustaría recibir respuestas / discusiones.

  1. Kent Beck utiliza una lista que agrega a la que se encamina para guiar el proceso de desarrollo. ¿Cómo se hace tal lista? Inicialmente tuve algunos elementos como "el servidor debería iniciarse", "el servidor debería abortar si el canal no está disponible", etc., pero se mezclaron y finalmente ahora, es algo así como "el cliente debería poder conectarse al servidor" (que inicio del servidor subsumido etc.).
  2. ¿Cómo manejas las reescrituras? Inicialmente seleccioné un sistema half duplex basado en tuberías con nombre para poder desarrollar la lógica de la aplicación en mi propia máquina y luego agregar la parte de comunicación USB. Se movió para convertirse en una cosa basada en sockets y luego se cambió de usar sockets en bruto a usar el módulo SocketServer de Python. Cada vez que las cosas cambiaban, descubrí que tenía que volver a escribir partes considerables de las pruebas, lo cual era molesto. Pensé que las pruebas serían una guía un tanto invariable durante mi desarrollo. Se sentían como más código para manejar.
  3. Necesitaba un cliente y un servidor para comunicarme a través del canal para probar cualquiera de los lados. Podría burlarme de uno de los lados para probar el otro, pero entonces no se probaría todo el canal y me preocupa que me lo pierda. Esto restó valor a todo el ritmo rojo / verde / refactor. ¿Esto es solo falta de experiencia o estoy haciendo algo mal?
  4. El tema "Falsearlo hasta que lo hagas" me dejó con un montón de códigos desordenados que luego dediqué mucho tiempo a refactorizar y limpiar. ¿Es así como funcionan las cosas?
  5. Al final de la sesión, ahora tengo a mi cliente y servidor ejecutando alrededor de 3 o 4 pruebas de unidad. Me tomó alrededor de una semana hacerlo. Creo que podría haberlo hecho en un día si estuviera usando las pruebas unitarias después del código. No consigo ver la ganancia.

Estoy buscando comentarios y consejos de personas que han implementado grandes proyectos no triviales completamente (o casi completamente) usando esta metodología. Para mí tiene sentido seguir el camino después de que ya tengo algo en ejecución y quiero agregar una nueva función, pero hacerlo desde el principio me parece aburrido y no merece la pena.

PD: Por favor, dime si esto debería ser un wiki de la comunidad y lo marcaré así.

Actualización 0 : Todas las respuestas fueron igualmente útiles. Escogí el que hice porque resonó más con mis experiencias.

Actualización 1 : ¡Practica, practica, practica!


  1. Kent Beck usa una lista ... finalmente, ahora es algo como "el cliente debería poder conectarse al servidor" (que incluye el inicio del servidor, etc.).

A menudo una mala práctica.

Pruebas separadas para cada capa separada de la arquitectura son buenas.

Las pruebas consolidadas tienden a ocultar los problemas arquitectónicos.

Sin embargo, solo prueba las funciones publicas. No todas las funciones.

Y no inviertas mucho tiempo optimizando tus pruebas. La redundancia en las pruebas no duele tanto como lo hace en la aplicación de trabajo. Si las cosas cambian y una de las pruebas funciona, pero se rompe otra, quizás pueda refactorizar sus pruebas. No antes.

2. ¿Cómo manejas las reescrituras? ... Encontré que tuve que reescribir partes considerables de las pruebas.

Estás probando a un nivel de detalle demasiado bajo. Probar la interfaz externa, pública y visible. La parte que se supone que es inmutable.

Y

Sí, un cambio arquitectónico significativo significa un cambio significativo en las pruebas.

Y

El código de prueba es cómo pruebas que funcionan las cosas. Es casi tan importante como la propia aplicación. Sí, es más código. Sí, debes manejarlo.

3. Necesitaba un cliente y un servidor para comunicarme a través del canal para probar cualquiera de los lados. Podría burlarme de uno de los lados para probar el otro, pero entonces no se probaría todo el canal ...

Hay pruebas unitarias. Con burlas.

Hay pruebas de integración, que ponen a prueba todo el asunto.

No los confundas.

Puedes usar herramientas de prueba unitaria para hacer pruebas de integración, pero son cosas diferentes.

Y necesitas hacer ambas cosas.

4. El tema "Falsearlo hasta que lo hagas" me dejó con un montón de códigos desordenados que luego dediqué mucho tiempo a refactorizar y limpiar. ¿Es así como funcionan las cosas?

Sí. Así es exactamente como funciona. A la larga, a algunas personas les resulta más eficaz que esforzarse el cerebro tratando de hacer todo el diseño por adelantado. A algunas personas no les gusta esto y quieren hacer todo el diseño por adelantado; eres libre de hacer un montón de diseño por adelantado si quieres.

Descubrí que la refactorización es algo bueno y el diseño desde el principio es demasiado difícil. Tal vez sea porque he estado programando durante casi 40 años y mi cerebro se está agotando.

5. No logro ver la ganancia.

Todos los verdaderos genios encuentran que las pruebas las ralentizan.

El resto de nosotros no podemos estar seguros de que nuestro código funcione hasta que tengamos un conjunto completo de pruebas que demuestren que funciona.

Si no necesita pruebas de que su código funciona, no necesita pruebas.


¿Cómo se hace una lista para agregar y eliminar para guiar el proceso de desarrollo? Inicialmente tuve algunos elementos como "el servidor debería iniciarse", "el servidor debería cancelarse si el canal no está disponible"

Los elementos en las listas de TDD TODO están más detallados que eso, apuntan a probar un solo comportamiento de un método, por ejemplo:

  • probar la conexión exitosa del cliente
  • tipo de error de conexión cliente de prueba 1
  • error de conexión del cliente de prueba tipo 2
  • prueba la comunicación exitosa con el cliente
  • la comunicación del cliente de prueba falla cuando no está conectado

Podría crear una lista de pruebas (positivas y negativas) para cada ejemplo que dio. Además, cuando realiza pruebas de unidad, no establece ninguna conexión entre el servidor y el cliente. Simplemente invoca métodos de forma aislada, ... Esto responde a la pregunta 3.

¿Cómo manejas las reescrituras?

Si la prueba de unidad prueba el comportamiento y no la implementación, no es necesario volver a escribirlos. Si el código de prueba de la unidad realmente crea una tubería con nombre para comunicarse con el código de producción y, obviamente, las pruebas deben modificarse al cambiar de tubería a zócalo. Las pruebas unitarias deben mantenerse alejadas de los recursos externos, como sistemas de archivos, redes, bases de datos, ya que son lentas, pueden no estar disponibles ... consulte estas reglas de Pruebas unitarias .

Esto implica que la función de nivel más bajo no se prueba en la unidad, se probará con pruebas de integración, donde todo el sistema se prueba de extremo a extremo.


P. Kent Beck usa una lista que agrega y separa para guiar el proceso de desarrollo. ¿Cómo se hace tal lista? Inicialmente tuve algunos elementos como "el servidor debería iniciarse", "el servidor debería abortar si el canal no está disponible", etc., pero se mezclaron y finalmente ahora, es algo así como "el cliente debería poder conectarse al servidor" (que inicio del servidor subsumido etc.).

Comienzo por elegir cualquier cosa que pueda revisar. En su ejemplo, eligió "el servidor se inicia".

Server starts

Ahora busco cualquier prueba más simple que quiera escribir. Algo con menos variación, y menos partes móviles. Podría considerar "servidor configurado correctamente", por ejemplo.

Configured server correctly Server starts

Sin embargo, en realidad, "el servidor se inicia" depende de "el servidor configurado correctamente", por lo que hago que el enlace quede claro.

Configured server correctly Server starts if configured correctly

Ahora busco variaciones. Yo pregunto: "¿Qué podría salir mal?" Podría configurar el servidor incorrectamente. ¿Cuántas maneras diferentes que importan? Cada uno de esos hace una prueba. ¿Cómo podría no iniciarse el servidor aunque lo haya configurado correctamente? Cada caso de eso hace una prueba.

P. ¿Cómo manejas las reescrituras? Inicialmente seleccioné un sistema half duplex basado en tuberías con nombre para poder desarrollar la lógica de la aplicación en mi propia máquina y luego agregar la parte de comunicación USB. Se movió para convertirse en una cosa basada en sockets y luego se cambió de usar sockets en bruto a usar el módulo SocketServer de Python. Cada vez que las cosas cambiaban, descubrí que tenía que volver a escribir partes considerables de las pruebas, lo cual era molesto. Pensé que las pruebas serían una guía un tanto invariable durante mi desarrollo. Se sentían como más código para manejar.

Cuando cambio de comportamiento, me parece razonable cambiar las pruebas, ¡e incluso cambiarlas primero! Sin embargo, si tengo que cambiar las pruebas que no verifican directamente el comportamiento que estoy cambiando, eso es una señal de que mis pruebas dependen de demasiados comportamientos diferentes. Esas son pruebas de integración, que creo que son una estafa. (Google "Las pruebas de integración son una estafa")

P. Necesitaba que un cliente y un servidor se comunicaran a través del canal para probar cualquiera de los lados. Podría burlarme de uno de los lados para probar el otro, pero entonces no se probaría todo el canal y me preocupa que me lo pierda. Esto restó valor a todo el ritmo rojo / verde / refactor. ¿Esto es solo falta de experiencia o estoy haciendo algo mal?

Si construyo un cliente, un servidor y un canal, entonces trato de verificar cada uno de forma aislada. Comienzo con el cliente, y cuando lo pruebo, decido cómo deben comportarse el servidor y el canal. Luego implemento el canal y el servidor para que coincidan con el comportamiento que necesito. Al verificar el cliente, apago el canal; Al comprobar el servidor, me burlo del canal; Al revisar el canal, apago y me burlo del cliente y del servidor. Espero que esto tenga sentido para usted, ya que tengo que hacer algunas suposiciones serias sobre la naturaleza de este cliente, servidor y canal.

P. "Falso hasta que lo logres" me dejó con un montón de códigos desordenados que luego dediqué mucho tiempo a refactorizar y limpiar. ¿Es así como funcionan las cosas?

Si dejas que tu código "falso" se ensucie mucho antes de limpiarlo, es posible que hayas pasado mucho tiempo fingiéndolo. Dicho esto, encuentro que aunque termino limpiando más código con TDD, el ritmo general se siente mucho mejor. Esto viene de la práctica.

P. Al final de la sesión, ahora tengo a mi cliente y servidor ejecutando alrededor de 3 o 4 pruebas de unidad. Me tomó alrededor de una semana hacerlo. Creo que podría haberlo hecho en un día si estuviera usando las pruebas unitarias después del código. No consigo ver la ganancia.

Debo decir que, a menos que su cliente y servidor sean muy, muy simples, necesita más de 3 o 4 pruebas para verificarlos a fondo. Supongo que sus pruebas verifican (o al menos ejecutan) una serie de comportamientos diferentes a la vez, y eso podría explicar el esfuerzo que le llevó escribirlos.

Además, no mida la curva de aprendizaje. Mi primera experiencia real en TDD consistió en volver a escribir el trabajo de 3 meses en 9, 14 horas al día. Tuve 125 pruebas que tardaron 12 minutos en correr. No tenía idea de lo que estaba haciendo y me sentía lento, pero se sentía estable y los resultados eran fantásticos. Básicamente reescribí en 3 semanas lo que originalmente tomó 3 meses para equivocarme. Si lo escribiera ahora, probablemente podría hacerlo en 3-5 días. ¿La diferencia? Mi conjunto de pruebas tendría 500 pruebas que demoran entre 1 y 2 segundos en ejecutarse. Eso vino con la práctica.


Como comentario preliminar, TDD toma práctica. Cuando miro hacia atrás en las pruebas que escribí cuando comencé TDD, veo muchos problemas, como cuando miro el código que escribí hace unos años. Sigue haciéndolo, y al igual que comienzas a reconocer el código bueno del malo, sucederán las mismas cosas con tus pruebas, con paciencia.

¿Cómo se hace tal lista? Inicialmente tuve algunos elementos como "el servidor debería iniciarse", "el servidor debería abortar si el canal no está disponible", etc., pero se mezclaron y, finalmente, ahora es algo como "el cliente debería poder conectarse al servidor"

"La lista" puede ser bastante informal (ese es el caso en el libro de Beck), pero cuando pasa a hacer los artículos en pruebas, intente escribir las declaraciones en un "[Cuando algo le sucede a esto] entonces [esta condición debería ser cierta en que] "formato. Esto lo forzará a pensar más sobre qué es lo que está verificando, cómo lo verificaría y se traduciría directamente a las pruebas, o si no lo hace, debería darle una pista sobre qué pieza de funcionalidad falta. Piense caso de uso / escenario. Por ejemplo, "el servidor debería iniciarse" no está claro, porque nadie está iniciando una acción.

Cada vez que las cosas cambiaban, descubrí que tenía que volver a escribir partes considerables de las pruebas, lo cual era molesto. Pensé que las pruebas serían una guía un tanto invariable durante mi desarrollo. Se sentían como más código para manejar.

Primero, sí, las pruebas son más códigos y requieren mantenimiento, y la escritura de pruebas mantenibles requiere práctica. Estoy de acuerdo con S. Lott, si necesita cambiar mucho sus pruebas, probablemente esté probando "demasiado profundo". Lo ideal es que desee probar en el nivel de la interfaz pública, que no es probable que cambie, y no en el nivel de detalle de la implementación, que podría evolucionar. Pero parte del ejercicio se trata de crear un diseño, por lo que debe esperar que algo salga mal y que también tenga que mover / refactorizar sus pruebas.

Podría burlarme de uno de los lados para probar el otro, pero entonces no se probaría todo el canal y me preocupa que me lo pierda.

No estoy totalmente seguro de eso. Por lo que parece, usar una imitación fue la idea correcta: tomar un lado, burlarse del otro, y verificar que cada lado funcione, asumiendo que el otro esté implementado correctamente. La prueba de todo el sistema en conjunto es una prueba de integración, que también desea hacer, pero que normalmente no forma parte del proceso TDD.

El tema "Falsearlo hasta que lo hagas" me dejó con un montón de códigos desordenados que luego dediqué mucho tiempo a refactorizar y limpiar. ¿Es así como funcionan las cosas?

Debes pasar mucho tiempo refactorizando mientras haces TDD. Por otro lado, cuando lo falsifica, es temporal, y su próximo paso inmediato debería ser deshacerlo. Por lo general, no debes pasar varias pruebas porque lo falsificaste: deberías centrarte en una pieza a la vez y trabajar en la refactorización lo antes posible.

Creo que podría haberlo hecho en un día si estuviera usando las pruebas unitarias después del código. No consigo ver la ganancia.

Nuevamente, se necesita práctica, y debes ir más rápido con el tiempo. Además, a veces TDD es más fructífero que otros, me parece que en algunas situaciones, cuando sé exactamente el código que quiero escribir, es más rápido escribir una buena parte del código y luego escribir pruebas.
Además de Beck, un libro que disfruté es The Art of Unit Testing, de Roy Osherove. No es un libro de TDD, y está orientado a .Net, pero puede que quiera echarle un vistazo de todos modos: una buena parte es sobre cómo escribir pruebas de mantenimiento, calidad de pruebas y preguntas relacionadas. Encontré que el libro resonó con mi experiencia después de haber escrito las pruebas y, a veces, luché por hacerlo bien ...
Así que mi consejo es que no arrojes la toalla demasiado rápido y dale algo de tiempo. También puede querer darle una oportunidad en algo más fácil: ¡probar cosas relacionadas con la comunicación del servidor no parece ser el proyecto más fácil para comenzar!


Como programador novato, lo que me pareció difícil acerca del desarrollo basado en pruebas fue la idea de que las pruebas deberían ser lo primero.

Para el principiante, eso no es realmente cierto. El diseño es lo primero. (Interfaces, objetos y clases, métodos, lo que sea apropiado para su idioma). Luego escriba sus pruebas para eso. Luego escribes el código que realmente hace cosas.

Ha pasado un tiempo desde que miré el libro, pero Beck parece escribir como si el diseño del código simplemente pasara inconscientemente en tu cabeza. Para programadores experimentados, eso puede ser cierto, pero para noobs como yo, nuh-uh.

Los primeros capítulos de Code Complete me parecieron muy útiles para pensar en el diseño. Enfatizan el hecho de que su diseño puede cambiar, incluso una vez que esté en el nivel mínimo de implementación. Cuando eso suceda, es posible que tenga que volver a escribir sus pruebas, ya que se basaron en los mismos supuestos de su diseño.

La codificación es difícil. Vamos de compras.


Las tuberías con nombre se colocaron detrás de la interfaz correcta, cambiando la forma en que se implementa esa interfaz (de tuberías con nombre a sockets a otra biblioteca de sockets) solo debería afectar las pruebas para el componente que implementa esa interfaz. Por lo tanto, recortar las cosas de forma más / diferente hubiera ayudado ... Esa interfaz a la que se encuentran los conectores probablemente evolucionará a.

Empecé a hacer TDD tal vez hace 6 meses? Todavía estoy aprendiendo a mí mismo. Puedo decir con el tiempo que mis pruebas y código han mejorado mucho, así que sigue así. Realmente recomiendo el libro XUnit Design Patterns también.


Para el punto uno, vea una question que le pregunté hace un tiempo relacionada con su primer punto.

En lugar de manejar los otros puntos a su vez, ofreceré un consejo global. Práctica. Me tomó un buen rato y algunos proyectos "poco fiables" (aunque personales) para obtener TDD. Solo Google por razones mucho más convincentes sobre por qué TDD es tan bueno.

A pesar de las pruebas que condujeron al diseño de mi código, todavía obtengo una pizarra y escribo algunos diseños. A partir de esto, al menos tienes una idea de lo que debes hacer. Luego produzco la lista de pruebas por accesorio que creo que necesito. Una vez que comience a trabajar, se agregarán más funciones y pruebas a la lista.

Una cosa que se destacó de su pregunta es el hecho de volver a escribir sus pruebas nuevamente. Esto suena como que estás llevando a cabo pruebas de comportamiento, en lugar de estado. En otras palabras, las pruebas parecen demasiado ligadas a su código. Por lo tanto, un simple cambio que no afecte la salida romperá algunas pruebas. La prueba de unidad (al menos una buena prueba de unidad) también es una habilidad para dominar.

Recomiendo Google Testing Blog bastante porque algunos de los artículos allí hicieron que mis pruebas para proyectos TDD fueran mucho mejores.