Beneficios y caraterísticas de un buen test unitario y de TDD

He leído un pequeño artículo sobre recomendaciones sobre Test-Driven Development: Guidelines for Test-Driven Development de Jeffrey Palermo. Me han llamado la atención sus apuntes sobre los beneficios de TDD y su descripción de un buen test unitario, así que las he traducido. Yo no soy muy partidario de TDD, pues me parece demasiado complejo el poder escribir los test antes, pero si soy muy partidario del testeo unitario.


Benefícios de Test-Driven Development



  • El conjunto de test unitarios proporciona contante retroalimentación de que cada uno de los componentes sigue funcionando.
  • Los test unitarios actuan como documentación que no se queda obsolet, al contrarío que otros tipos de documentación.
  • Cuando el test pasa y el código de producción es refactorizado para eliminar duplicidades, es claro que el código está terminado, y el desarrollador se puede mover a la siguiente tarea.
  • TDD fuerza un análisis y diseño crito porque el desarrollador no puede crear código de producción sin entender realmente cuales deberían ser los resultado deseados y como probarlos.
  • El software tiende a estar mejor diseñado, esto es, menos acoplado y más facilmente mantenible, porque el desarrollador es libre de hacer decisiones de diseño y refactorizar en cualquier momento con la confianza de que el software todavia funciona.
  • El conjunto de tests actua como una red de seguridad contra regresiones en los bugs: Si se encuentra un bug, el desarrollador debe crear un test que ponga de manifiesto el bug y despues modificar el código de producción para eliminar el bug. En sucesivas ejecuciones de los test, todas las correcciones de bugs son verificadas.
  • El tiempo de depuración se reduce.

Características de un buen unit test



  • Se ejecuta rápido, se ejecuta rápido, se ejecuta rápido. Si los test son lentos, no se ejecutaran a menudo.
  • Separa o simula dependencias ambientales como bases de datos, sistemas de archivos, redes, colas y demás. Los test que ejercitan estos no serán rápidos y los fallos no dan información significativa sobre cual es el problema realmente.
  • Es muy limitado en su alcance. Si el test falla, es obvio donde bucar el problema.  Realiza pocas llamadas a la clase Assert de manera que el código que falla sea obvio. Es importante probar una única cosa en cada test. 
  • Se ejecuta y pasa de manera independiente. Si los tests requieren establecer entorno especial o fallan inexperadamente, no son buenos tests unitarios. Cambialos por simplicidad y fiabilidad. Los teste deben ejecutarse y pasarse en cualquier máquina. La excusa «funciona en mi máquina» no sirve.
  • Usa a menudo stubs y mock objects. Si el código que está siendo probado llama a un base de datos o al sistema de archivos, estas dependencias deben ser simuladas. Esta dependencias habitualmente serán abstraidas usando interfaces.
  • Revela claramente su intención. Otro desarrollador puede ver el test y comprender que se espera que haga el código de producción.

13 comentarios sobre “Beneficios y caraterísticas de un buen test unitario y de TDD”

  1. Pero que pasa cuando no tienes clara la arquitectura a utilizar ?, recientemento he empezado a trabajar con pruebas unitarias y uno de los problemas con los que me encuentro es el de tener que reacerlas constatemente, cuando no tienes claro el diseño de una aplicación normalmente vas aproximandote hacia esta, en varios pasos, y esto te obliga a tener que rehacer de forma constante tu código y de la misma forma las pruebas unitarias, y el tiempo que se pierde, buff…, no se si al final compensara…, estoy convencido de que de es gran ayuda tener una bateria de pruebas unitarias, pero cuando los desarrollos van ha sufrir muchos cambios a lo largo del proceso, quizas la solución este en escribirlas al final. Aunque recuerdo alguien que me dijo «en ese caso, seguro que no realizaras las pruebas, una vez hayas terminado la aplicación». Creo que todavian deben mejorar los sistemas para soportar pruebas unitarias, es mas estoy convencido que en poco tiempo muchas de las pruebas que realizamos podran generarse de forma automática por el sistema, pero tambien creo que debemos avanzar en este sentido para mejorar la calidad del software aunque todavia tengo muchas dudas, quien debe desarrollar las pruebas ?? los programadores, expertos en testing… además, la parte de los mock objects, todavía deja mucho que desear es muy costosa y muchas veces te obliga cambiar el desarrollo para poder testearlo.

  2. La práctica habitual es no comenzar a realizar testeo unitario completo hasta que se ha trazado la línea base de la arquitectura.

    Esa línea base de la arquitectura se construye realizando la implementación de una fina tira vertical de la aplicación que ejercite un escenario básico, pero lo más completo posible desde el punto de vista de la arquitectura de la aplicación. Esto permite validar tus decisiones arquitectónicas y asegurar que son realmente viables e implenmentables. Además sirve como base clara para la evolución y ampliación de la arquitectura.

    La construcción de esta línea base, se suele llamar fase cero o iteración cero en las metodologías ágiles y es una manera muy eficiente de reducir los riesgos asociados a una arquitectura inadecuada.

    Es normal que tu arquitectura varíe mucho hasta que se establece una línea base, pero no debería sufrir grandes cambios, sino una continua evolución, una vez establecida esta línea base.

    Evidentemente una vez tenemos una línea base estabilizada debemos escribir pruebas unitarias para esta y continuar todo el desarrollo escribiendo las pruebas en paralelo al desarrollo de funcionalidad.

  3. Aunque entiendo a Juan perfectamente, creo el problema de no entender la necesidad de las pruebas unitarias es debido principalmente a un “erróneo” enfoque de lo que son las iteraciones. Debemos empezar a pensar que iteración es algo así como requisitos+código+test+loquieranustedes.

    Entiendo que en cada iteración debería entrar la codificación de requerimientos, la creación de las pruebas unitarias (si es antes o después de codificar los requerimientos no me meto), y un chequeo del cliente/responsable/lo que sea. Una vez concluido el plazo se entra en la siguiente iteración donde:

    -Podemos cambiarlo todo porque no es lo que se pedía: Es una evolución donde tendremos que reescribir algunos métodos y por supuesto sus test.

    -Añadimos nueva funcionalidad: tendremos que escribir nuevas “cosas” y adaptar los anteriores clases para que encajen lo nuevo. Una vez más hay que adaptar las pruebas unitarias.

    Al final de cada iteración podemos lanzar los test unitarios y ver que el código generado/adaptado en esa iteración y en todas las otras iteraciones posteriores es correcto. Es la única forma de asegurarse que estamos haciendo las cosas con pruebas demostrables. Dejarlos todos para el final es una autentica locura porque ¿a quién se le ocurriría, por ejemplo, escribir más de 1000 test de una tirada? Es un trabajo tan tedioso que yo no conozco a nadie que tenga tanta paciencia para hacerlo con calidad (sin dejarse nada de lado). Y ya que hablamos de calidad, lo test unitarios son una prueba (necesaria) de que hemos hecho un software de calidad, ¿no? Aunque evidentemente para hacer un software de calidad tenemos que tener en cuenta muchas más cosas…

    Saludos

  4. Sin duda dejar los test para el final no tiene ningún sentido. Los test nos ayudan a detectar errores y cuanto más temprano se detecta un error en un sistema informatico más económica (en tiempo o dinero) resulta su corrección.

    Los test unitarios de lo que nos protegen principalmente es de las regresiones (código que funcionaba y debido a cambios deja de funcionar) y de los efectos laterales (cambios que rompen partes del programa que nosotros ni siquiera hemos imaginado que están relacionadas con el cambio). Cuando más nos ayudan los test unitarios es durante el desarrollo, que es cuando más cambios se producen. Además los test nos proporcionan otras ventajas una vez terminada la fase de construcción, pero estas no son suficientes, en mi opinión, para justificar por si mismas la costrucción de test a posteriori.

    De todos modos, como bien dice Miguel Angel, pasar los test unitarios con una buena cobertura no es garantia de calidad, podemos pasar todos los test y que nuestro programa no cubra las necesidades de nuestros clientes. Por ello es necesario que cada escenario, tenga aparejada una prueba de aceptación, que mida la calidad desde el punto de vista del cliente. Un buen enfoque es escribir las pruebas de aceptación a la vez que es escenario. Si sabes como probar algo es que lo has entendido. Así como para el código no veo claro el enfoque ‘test first’ si me parece mucho más útil para los escenarios.

  5. Bueno, el crear los test unitarios antes que las funcionalidades obliga a los programadores a que piensen antes de programar, pero que piensen en resultados (que es lo que se pide) y en las formas.

    Muchos desarrolladores (incluidos los míos) se ponen a programar con la idea de lo que tienen que hacer. Muchas veces esta idea es a nivel de Interfaz de usuario o de resultados concretos, sin haber pensado antes qué patrones de diseño deben aplicar, qué clases, qué interfaces, etc.. Al obligar a realizarse antes los test unitarios obligamos a que piensen en la implementación que tienen que hacer: Resultados + Forma de lograrlo.

    Además sirve para autoestimarse en tiempo. Un desarrollador debe tener el buen hábito de, a primera hora de la jornada, ponerse una meta de un x por ciento de los test para el fin de la jornada. Usar esta métrica le ayudará a demostrar y justificar su trabajo.

    Evidentemente hay programadores que necesiten que se le inculquen este tipo de reglas y otros que son lo suficientemente disciplinados para no necesitarlos porque piensan como deben de pensar y saben cuánto tiempo les lleva cada cosa. Pero todos fallamos alguna vez (y muchas) y para eso están los métodos, aunque siempre hay que buscar el equilibrio entre la opresión del método y la libertad artística del programador.

  6. Estoy deacuerdo con todos vosotros, pero pienso en el tiempo invertido, en mi caso mi equipo de desarrollo es muy pequeño y tengo que variar mucho el diseño, pues hay que hacer todo el trabajo, incluido el analisis, el problema radica en que muchas veces el cliente no tiene clara las necesidades, y vas implementado nueva funcionalidad a medida que ven funcionando la aplicación, pues es en ese momento y solo entonces cuando se dan cuenta de que falta esto o lo otro, se que este es un error muy común a la hora de diseñar aplicaciones, pero seamos serios, es mi realidad y la de muchos desarrolladores, enfrentarse a proyectos con poca gente, muy largos en los que las especificaciones cambian acompañadas al desarrollo, esto me obliga a perder mucho tiempo con las pruebas unitarias, por ejemplo cuando incluyo un campo nuevo en un procedimiento almacenado o simplemente cuando inserto un campo nuevo en una tabla.
    Entiendo que al final lograremos un código de mas calidad, pero a veces hay que balancear para conseguir un buen rendimiento. Ademas si realizo las pruebas unitarias una vez he trazado la linea básica del proyecto, pierdo una de las premisas de Agile, que te aconseja realizar la pruebas unitarias antes del código.

    Otro de los problemas con los que me he encontrado es que algunas veces las pruebas unitarias tienden a alterar mi codigo, por ejemplo en el caso de los mock, no puedo utilizar pruebas unitarias con clases estaticas ya que tengo que implementar un interface, asi que las pruebas estan condicionando mi código.

    Pero sigo creyendo que son un primer paso para mejorar la calidad del código, aunque pienso todavia tienen que evolucionar bastante y son dificiles de adaptar en entornos de desarrollo similares al mio.

  7. La idea de hacer test unitarios es muy buena, de echo me sirvio mucho utilizar pequeños test unitarios al principio de un proyecto porque me da la fiabilidad de que las clases que estoy utilizando funcionan correctamente y ademas me proporcionan la posibilidad de capturar el error si es que este llegara a existir y desplegarlo, sin duda es una gran herramienta pero algo dificil de implementar, deberia existir herramientas mas sencillas de manejar y un poco mas eficientes.

  8. No estoy de acuerdo con Miguel Angel Ramos en lo siguiente

    »
    -Podemos cambiarlo todo porque no es lo que se pedía: Es una evolución donde tendremos que reescribir algunos métodos y por supuesto sus test.

    -Añadimos nueva funcionalidad: tendremos que escribir nuevas “cosas” y adaptar los anteriores clases para que encajen lo nuevo. Una vez más hay que adaptar las pruebas unitarias.
    »
    Basicamente estas diciendo que al agregar/modificar alguna funcionalidad habra que modificar/adaptar las pruebas, en mi opinion eso proviene de un concepto erroneo, porque una prueba lo unico que hace es testear una funcionalidad en un momento del desarrollo del software. Y una vez agregada y testeada esa funcionalidad no hay razon para eliminarla del sistema (ya sea simplemente quitandola o modificandola), por lo tanto en un desarrollo nunca se quitara una funcionalidad… y no habria motivos para modificar un test

    Ej, si se le agrega un campo a un stored procedure, o un argumento nuevo a un metodo… es cierto que hay que escribir los tests para la nueva funcionalidad, pero los tests preexistentes de ese metodo se mantienen identicos (sin la mas minima modificacion) ya que la funcionalidad que tenia hasta entonces no se eliminara.
    Si fuera el caso de tener que cambiar el comportamiento de alguna clase o metodo, tambien seria incompatible con esa logica, y se deberia definir otra clase u otro metodo distinto y mantener los originales como estaban.
    Repito, esto que digo gira en torno a que el desarrollo siempre crezca funcionalmente, es decir, que nunca se elimine o deshabilite una funcionalidad a drede. No estoy diciendo que por el hecho de usar TDD esto tenga que ser asi…, mas bien es el TDD el que esta avisando de que esto conviene que sea asi

    Todo lo demas que dijiste me parecio muy acertado, sobre todo la idea de no escribir las 1000 pruebas al final, sino hacerlo en cada momento para cada funcionalidad

  9. No estoy de acuerdo con Miguel Angel Ramos en lo siguiente

    »
    -Podemos cambiarlo todo porque no es lo que se pedía: Es una evolución donde tendremos que reescribir algunos métodos y por supuesto sus test.

    -Añadimos nueva funcionalidad: tendremos que escribir nuevas “cosas” y adaptar los anteriores clases para que encajen lo nuevo. Una vez más hay que adaptar las pruebas unitarias.
    »
    Basicamente estas diciendo que al agregar/modificar alguna funcionalidad habra que modificar/adaptar las pruebas, en mi opinion eso proviene de un concepto erroneo, porque una prueba lo unico que hace es testear una funcionalidad en un momento del desarrollo del software. Y una vez agregada y testeada esa funcionalidad no hay razon para eliminarla del sistema (ya sea simplemente quitandola o modificandola), por lo tanto en un desarrollo nunca se quitara una funcionalidad… y no habria motivos para modificar un test

    Ej, si se le agrega un campo a un stored procedure, o un argumento nuevo a un metodo… es cierto que hay que escribir los tests para la nueva funcionalidad, pero los tests preexistentes de ese metodo se mantienen identicos (sin la mas minima modificacion) ya que la funcionalidad que tenia hasta entonces no se eliminara.
    Si fuera el caso de tener que cambiar el comportamiento de alguna clase o metodo, tambien seria incompatible con esa logica, y se deberia definir otra clase u otro metodo distinto y mantener los originales como estaban.
    Repito, esto que digo gira en torno a que el desarrollo siempre crezca funcionalmente, es decir, que nunca se elimine o deshabilite una funcionalidad a drede. No estoy diciendo que por el hecho de usar TDD esto tenga que ser asi…, mas bien es el TDD el que esta avisando de que esto conviene que sea asi

    Todo lo demas que dijiste me parecio muy acertado, sobre todo la idea de no escribir las 1000 pruebas al final, sino hacerlo en cada momento para cada funcionalidad

Responder a anonymous Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *