unit test software pruebas por guiado explica driven development desarrollo conducido comportamiento agil unit-testing testing tdd

unit-testing - software - unit test driven development



Preguntas filosóficas sobre el desarrollo guiado por pruebas (8)

Siempre me ha intrigado el desarrollo basado en pruebas, pero nunca puedo seguir adelante cuando lo pruebo en proyectos reales. Tengo un par de preguntas filosóficas que surgen continuamente cuando lo intento:

  1. ¿Cómo manejas los grandes cambios? Cuando se trata de probar funciones individuales (algunos parámetros, un valor de resultado, pocos efectos secundarios), TDD es una obviedad. Pero, ¿qué pasa cuando necesita revisar a fondo algo grande, por ejemplo, cambiar de una biblioteca de análisis SAX a una biblioteca de análisis DOM? ¿Cómo se mantiene el ciclo de prueba-código-refactor cuando su código está en un estado intermedio? Una vez que empieces a hacer el cambio, obtendrás una gran cantidad de pruebas fallidas hasta que hayas terminado completamente la revisión (a menos que mantengas algún tipo de clase mestiza que use tanto DOM como SAX hasta que hayas terminado de convertir, pero eso es bastante extraño) . ¿Qué sucede con el ciclo de prueba-código-refactor de paso pequeño en este caso? Durante todo este proceso, ya no se moverá en pasos pequeños y totalmente probados. Debe haber alguna manera en que la gente trate esto.
  2. Al probar la GUI o el código de la base de datos con simulacros, ¿qué estás realmente probando? Las simulaciones están diseñadas para devolver exactamente la respuesta que desea, así que, ¿cómo sabe que su código funcionará con la base de datos del mundo real? ¿Cuál es el beneficio de las pruebas automatizadas para este tipo de cosas? Mejora un poco la confianza, pero a) no le da el mismo nivel de confianza que una prueba de unidad completa, yb) en cierta medida, ¿no está simplemente verificando que sus suposiciones funcionan con su código en lugar de Que tu código funciona con el DB o GUI?

¿Alguien puede indicarme buenos estudios de casos sobre el uso de desarrollo basado en pruebas en grandes proyectos? Es frustrante que básicamente solo puedo encontrar ejemplos de TDD para clases individuales.

¡Gracias!


Al probar la GUI o el código de la base de datos con simulacros, ¿qué estás realmente probando? Las simulaciones están diseñadas para devolver exactamente la respuesta que desea, así que, ¿cómo sabe que su código funcionará con la base de datos del mundo real? ¿Cuál es el beneficio de las pruebas automatizadas para este tipo de cosas? Mejora un poco la confianza, pero a) no le da el mismo nivel de confianza que una prueba de unidad completa, yb) en cierta medida, ¿no está simplemente verificando que sus suposiciones funcionan con su código en lugar de Que tu código funciona con el DB o GUI?

Este es mi enfoque: para la capa de acceso a la base de datos (DAL), no uso simulacro para mi prueba de unidad. En su lugar, ejecuto las pruebas en una base de datos real, aunque una diferente a la base de datos de producción. Entonces, en este sentido, puedes decir que no ejecuto la prueba de unidad en la base de datos. Para las aplicaciones NHibernate, mantengo dos bases de datos con el mismo esquema, pero un tipo de base de datos diferente ( ORM hace fácil). Utilizo sqlite para mis pruebas automatizadas, y una base de datos MySQL o SQL real para pruebas ad-hoc.

Solo una vez usé el simulacro para la prueba unitaria del DAL; y fue entonces cuando estaba usando un conjunto de datos fuertemente tipado como ORM (¡un gran error!). La forma en que hice esto fue para que Typemock me devolviera una copia simulada de la tabla completa para que pueda realizar la select * en ella. Más tarde, cuando miré hacia atrás, deseé nunca hacer esto, pero eso fue hace mucho tiempo, y deseé haber usado un ORM adecuado.

En cuanto a la GUI, es posible realizar una prueba unitaria de la interacción de la GUI. La forma en que lo hice fue usar el patrón MVP para separar el Modelo, la Vista y el Presentador. En realidad, para este tipo de aplicación solo realizo pruebas en el Presenter y en el Modelo, en el que utilizo Typemock (o inyección de dependencia ) para aislar las diferentes capas, de modo que al mismo tiempo pueda concentrarme en una sola capa. No pruebo la vista, pero realizo muchas pruebas en Presenter (donde ocurren la mayoría de las interacciones y los errores).


Al probar la GUI o el código de la base de datos con simulacros, ¿qué estás realmente probando?

Por lo general, trato de separar la lógica de mi negocio, la lógica de visualización y el acceso a la base de datos. La mayoría de mis pruebas de unidades de GUI tienen que ver con la lógica de negocios. Aquí hay un ejemplo de pseudocódigo:

// Production code in class UserFormController: void changeUserNameButtonClicked() { String newName = nameTextBox.getText(); if (StringUtils.isEmpty(newName)) { errorBox.showError("User name may not be empty !"); } else { User user = engine.getCurrentUser(); user.name = newName; engine.saveUser(user); } } // Test code in UserFormControllerTest: void testValidUserNameChange() { nameTextBox = createMock(TextBox.class); expect(nameTextBox.getText()).andReturn("fred"); engine = createMock(Engine.class); User user = createMock(user); user.setName("fred"); expectLastCall(); expect(engine.getCurrentUser()).andReturn(user); engine.saveUser(user); expectLastCall(); replay(user, engine, nameTextBox); UserFormController controller = new UserFormController(); controller.setNameTextBox(nameTextBox); controller.setEngine(engine); controller.changeUserNameButtonClicked(); verify(user, engine, nameTextBox); } void testEmptyUserNameChange() { nameTextBox = createMock(TextBox.class); errorBox = createMock(ErrorBox.class); expect(nameTextBox.getText()).andReturn(""); errorBox.showError("User name may not be empty !"); expectLastCall(); replay(nameTextBox, errorBox); UserFormController controller = new UserFormController(); controller.setNameTextBox(nameTextBox); controller.setErrorBox(errorBox); controller.changeUserNameButtonClicked(); verify(nameTextBox, errorBox); }

Esto garantiza que, independientemente de lo dañada que esté mi base de datos y mi código GUI, al menos la lógica que controla el cambio de nombre de usuario funcione correctamente. Si organiza su código de GUI en un conjunto de controles individuales (o widgets o elementos de formulario o como se llame en su marco de GUI), puede probarlos de una manera similar.

Pero en última instancia, como usted dijo, estas pruebas unitarias no le darán toda la imagen. Para lograrlo, debe hacer lo que otros han sugerido: crear una base de datos real, con un "conjunto de datos" de oro, y ejecutar pruebas de integración / funcionales en su contra. Pero, en mi opinión, tales pruebas están fuera del alcance de TDD, porque su configuración suele requerir bastante tiempo.


  1. ¿Cómo manejas los grandes cambios?

    • Paso a paso. He trabajado en bastantes programas no triviales y siempre he podido dividir las cosas en pequeños cambios (que requieren horas y quizás días). Por ejemplo, reescribir un sitio web de 30Mpv se dividió en hacer una página a la vez. Esto se movía de un idioma a otro, escribiendo pruebas (pequeñas) a medida que avanzábamos, manteniendo el sitio actualizado con implementaciones frecuentes. En otro proyecto, convertimos una aplicación web GUI en un servidor back-end sin cabeza. Esto involucró muchos pasos pequeños a través de un mes o dos de trabajo y, finalmente, descartar gran parte del código web. Pero pudimos mantener todas las pruebas funcionando a medida que avanzábamos. Hicimos esto no porque intentáramos probar algo, sino porque era la mejor manera de reutilizar el código y las pruebas.

    • Los pasos más grandes pueden ser ayudados por pruebas con un alcance más amplio. Por ejemplo, su ejemplo de SAX-> DOM tendría una prueba de integración de alto nivel que verificará el comportamiento final. Sin embargo, cuando hice algo similar, escribí pruebas de comportamiento mucho más pequeñas en torno a los diferentes tipos de procesamiento de nodos, y la conversión de estos podría hacerse una por una.

  2. Al probar la GUI o el código de la base de datos con simulacros, ¿qué estás realmente probando?

    • Siempre debe asegurarse de que está escribiendo pruebas valiosas. Esto puede ser difícil. Es fácil, incluso si estás pensando, escribir algunas pruebas bastante redundantes.
    • Los simulacros no tienen sentido cuando intenta probar consultas de base de datos. Son útiles cuando está tratando de "simular" una capa debajo de lo que está probando ... por lo que son útiles en una prueba de controlador donde se burlará del comportamiento de la capa de servicio, que probará de forma independiente. Para probar las consultas de la base de datos, debe cargar la base de datos con los accesorios adecuados que probarán lo que está tratando de hacer. Esto se puede hacer con accesorios o con un código de configuración de prueba cuidadoso. Esto requiere un poco de reflexión para hacerlo bien, por lo que es bueno contar con un conjunto bien diseñado de datos de accesorios que le permitan escribir buenas pruebas de consulta de base de datos, cubriendo tantos casos importantes como sea posible.

    • Sí, está verificando sus suposiciones con simulacros, pero también debe probar esas suposiciones por separado. La alternativa, probarlos todos juntos, está bien, pero es más frágil. Significa que una prueba está probando más código y, por lo tanto, puede romperse más fácilmente.


En cuanto al ángulo de la base de datos, como mencionó Ngu Soon Hui, debe (IMHO) usar algo como DBUnit , que configurará la base de datos en una configuración conocida (para que pueda probar los resultados esperados) pero está usando la base de datos real que utiliza. Se utilizará la aplicación real.

Para cambios grandes, recomendaría crear una rama y permitir que las pruebas fallen. Esto le dará una lista de TODO de áreas que necesitan ser cambiadas, y podría argumentarse que aquí es donde TDD realmente brilla, incluso más que con las funciones pequeñas y aisladas.


En términos de manejo de grandes canales ... el propósito de TDD es probar los comportamientos de su código y cómo interactúa con los servicios de los que depende. Si deseaba utilizar TDD y estaba pasando de un analizador de DOM a un analizador de SAX y estaba escribiendo el analizador de saxo usted mismo, escribiría pruebas que verificaron el comportamiento del analizador de SAX basándose en una entrada conocida, es decir, un documento XML. El analizador de SAX puede depender de una colección de objetos de ayuda que, de hecho, podrían burlarse inicialmente para probar el comportamiento del analizador de SAX. Cuando estuviera listo para escribir el código de implementación para los objetos auxiliares, podría escribir pruebas sobre su comportamiento esperado basándose en una entrada conocida. En el ejemplo del analizador SAX, escribiría clases separadas para implementar este comportamiento a fin de no interferir con el código existente que tiene, que depende del analizador DOM. De hecho, lo que podría hacer es crear una interfaz IXMLParser que el analizador DOM y el analizador SAX implementen para que pueda cambiarlos a voluntad.

En lo que respecta al uso de simulacros o talones, la razón por la que usas un simulacro o un talón es que no estás interesado en probar el funcionamiento interno del simulacro o el talón, pero estás interesado en probar el funcionamiento interno de lo que depende de la simulacro o el talón y eso es lo que realmente estás probando desde la perspectiva de una unidad. Si está interesado en escribir pruebas de integración, debe escribir pruebas de integración y no pruebas unitarias. Me parece que escribir código de manera TDD es útil para ayudarme a definir la estructura y organización de mi código en torno al comportamiento que se me pide que proporcione.

No estoy familiarizado con ningún estudio de caso, pero estoy seguro de que están ahí fuera.


Mis 2 centavos ...

  1. si se rompen las pruebas porque cambió el tipo de analizador XML, indica que las pruebas son frágiles . Las pruebas deben especificar qué y no cómo . Lo que implica que, en este caso, las pruebas de alguna manera saben que está utilizando un motor de análisis SAX (un detalle de implementación); que no deberían. Solucione ese problema y debería estar mejor con grandes cambios.
  2. Cuando retira las GUI o las simulaciones de las pruebas a través de una interfaz, se asegura de que el sujeto de la prueba que utiliza las simulaciones (como el doble para los colaboradores reales) funcione según lo previsto. Puedes aislar errores en tu código de errores en tus colaboradores. Los simulacros te ayudan a mantener tu suite de pruebas rápido. También necesita pruebas que verifiquen que su colaborador real también se ajuste a la interfaz Y pruebas de que sus colaboradores reales están "conectados" correctamente.

¿Cómo manejas los grandes cambios?

Tan pequeño como sea necesario.

Algunas veces las refactorizaciones tienen una gran superficie pero son triviales en detalle. Esto se puede hacer en pasos bastante grandes. Poner demasiado esfuerzo en tratar de romperlos será un desperdicio.

Yo diría que un cambio de biblioteca XML está en esta categoría. Estás poniendo XML y obtienes algo de representación. Siempre que su representación no cambie (de un gráfico que representa el estado a un flujo de eventos), el cambio de biblioteca es fácil.

La mayoría de las refactorizaciones de tiempo no son triviales y deben ser desglosadas. El problema es cuándo hacer pasos más grandes y cuándo los más pequeños. Mi observación es que soy bastante malo al estimar el impacto de un cambio. La mayoría del software es lo suficientemente complicado como para que pueda pensar que su cambio es fácil de manejar, pero luego está toda la letra pequeña que debe funcionar nuevamente. Así que empiezo con una cantidad de cambio. Pero estoy preparado para revertir todo si comienza a ser impredecible. Yo diría que esto sucede en una de cada diez refactorizaciones. Pero este será difícil. Debe rastrear la parte del sistema que no se comporta como espera. El problema tiene que dividirse ahora en múltiples problemas más pequeños. Resuelvo un problema a la vez y verifico cuándo se hace. (Las repeticiones múltiples de revertir y dividir no son infrecuentes.)

Si cambia el analizador XML y la representación en su código, esto debería ser definitivamente al menos dos refactorizaciones separadas.

Prueba simulada

Estás probando un protocolo de comunicación entre objetos / capas con objetos simulados.

Todo el enfoque simulado puede considerarse como un modelo de comunicación como el modelo OSI . Cuando la capa X recibe una llamada con el parámetro x, llamará la capa Z con los parámetros ay b. Su prueba especifica este protocolo de comunicación.

Tan útil como puede ser la prueba simulada, pruebe la menor funcionalidad posible con ellos. La mejor opción son las pruebas basadas en estado: instalación del dispositivo, sistema de llamada bajo prueba, control del estado del sistema bajo prueba y pruebas funcionales puras (como en la programación funcional): la llamada con x devuelve a.

Intente diseñar su sistema de manera que la mayor parte de su funcionalidad esté acoplada libremente. Algunas de las funciones deben probarse con pruebas simuladas (un sistema completamente desacoplado es inútil).

Las pruebas de integración no son una opción para probar su sistema. Solo deben usarse para probar aspectos del sistema que pueden romper con la integración de múltiples unidades. Si intenta probar su sistema con pruebas de integración, entrará en el casino de permutación.

Así que su estrategia para la prueba de GUI debería ser clara. Las partes del código GUI que no se pueden probar de forma aislada se deben probar con pruebas simuladas (cuando se presiona este botón, se llama al servicio X con el parámetro y).

Las bases de datos enturbian el agua un poco. No puede simular una base de datos, a menos que vaya a volver a implementar el comportamiento de cada base de datos que le gustaría admitir. Pero esto no es una prueba de unidad, ya que está integrando un sistema externo. He hecho las paces con este problema conceptual y pienso en el DAO y la base de datos como una unidad inseparable que se puede probar con un enfoque de prueba basado en estado. (Lamentablemente, esta unidad se comporta de manera diferente cuando tiene su día de oráculo en comparación con su día de mysql. Y puede romperse en el medio y decirle que no puede hablar consigo misma).


Manejo de grandes cambios

En mi experiencia, estos son relativamente infrecuentes. Cuando suceden, la actualización de las pruebas es un problema menor. El truco es elegir la granularidad adecuada para las pruebas. Si prueba la interfaz pública, las actualizaciones irán rápidamente. Si prueba el código de implementación privada, el cambio de un analizador de SAX a un analizador de DOM será un gran momento y se sentirá como un dom. ;-)

Prueba de código GUI

En general no lo hago. Mantengo mi capa UI lo más delgada posible. La idea es probar lo que podría romper.

Código de base de datos de prueba

Cuando sea posible, prefiero colocar el código de acceso a los datos detrás de las interfaces y simularlo al probar la lógica de negocios. Como han mencionado otros, en algún momento es posible que desee ejecutar pruebas de integración contra el DAL para asegurarse de que funciona contra una base de datos de prueba en un estado conocido. Es posible que desee otras pruebas de integración de todo el sistema; Tener capas de diferentes tipos de pruebas es algo bueno. TDD es principalmente sobre diseño y no elimina la necesidad de pruebas de integración o aceptación.

Es muy posible abusar de simulacros y talones, escribiendo pruebas que no hacen más que probar objetos simulados. Se necesita mucha experiencia para escribir buenas pruebas; Todavía estoy aprendiendo.

Mi sugerencia sería seguir practicando TDD, tal vez en proyectos más pequeños inicialmente. Lea todo lo que pueda sobre él, hable con otros practicantes y use lo que le funcione.

La integración continua realmente ayuda con las pruebas, ya que asegura que las pruebas se ejecutan y hace visibles las pruebas interrumpidas. Muy recomendable.

EDITAR: Para ser franco, tengo problemas para desacoplar el código de acceso a los datos en muchos casos y termino usando bases de datos de prueba. Incluso las pruebas de integración como estas han demostrado ser valiosas, aunque son más lentas y más frágiles. Como dije, todavía estoy aprendiendo.