Tests Unitarios. Cuándo usarlos y pistas para conseguir un sistema robusto

27 Jul 2011

Introducción

En este artículo, quiero retomar el tema de la programación Javascript, o cualquiera otro lenguaje, mediante la metodología TDD.

Como desarrollador con más de 10 años años de experiencia en el mundo del software, tengo que reconocer que esta metodología funciona: aunque no he realizado ninguna medición estricta, la combinación de tests unitarios y un buen uso de la técnica Pomodoro ha incrementado mi productividad en un factor que debe rondar entre el x7 y x10. Además de esto, mis códigos son mucho más limpios, manejables y, sobre todo, presentan menos bugs.

En el último post, Tú no sabes programar, algunos de los comentarios que más llamaron mi atención fueron aquellos que descalificaban la afirmación de que el TDD es algo que un programador serio debe practicar. Estas críticas tienen que estar basadas, casi sin duda alguna, en el desconocimiento real y/o práctico de lo que supone esta metodología. Y esto viene de lo quizá antiintuivo del método:

  • Escribir un test previo a cualquier código. Además, este test debería poner a prueba una mínima porción de funcionalidad. Para quienes se inician en esta metodología, esto resulta muy chocante. ¿Cómo probar algo que aún no se ha definido? ¿No resulta quizá algo absurdo? Y, ¿por qué testearlo hasta algo hasta su más mínima expresión aunque resulte tan obvio?
  • Escribir el mínimo código necesario para que el test se supere. Esto también resulta complicado de seguir a rajatabla al principio: somos programadores, tenemos casi todo el código que necesitamos implementar en la cabeza; ¿por qué tengo que ir a pasos tan ridículamente cortos en lugar de avanzar más deprisa y solo detenernos en aquellos más complejos?
  • Refactorizar en cada iteración. Esta norma es algo más relajada; solo tendríamos que refactorizar en el caso de que realmente sea necesario y estemos detectando malos olores. Pero hay que refactorizar en el momento que sea necesario y no dejarlo para más tarde ya que, como afirma la ley de LeBlanc, ‘más tarde significa nunca’.

Y sin embargo… funciona! Es cierto que cuesta aplicar en un principio; y además no siempre el resultado es el esperado. Porque, una de las cosas que nunca debemos olvidar es que, tanto el éxito como el fracaso de este sistema, radica en su elemento más pequeño: los tests unitarios. Y ese es el tema principal de esta entrada: buenas y malas prácticas a la hora de escribir tests unitarios.

Diferencia entre buenos y malos tests

No es fácil escribir buenos tests; de hecho, según el tío Bob, no todo el mundo es capaz de hacerlo: es una cualidad con la que algunos programadores nacen y que a otros les cuesta un gran esfuerzo aprender. La idea detrás de esta premisa es que podemos ser unos brillantes desarrolladores, con décadas de experiencia, pero no por ello tenemos que saber escribir buenos tests unitarios sin la práctica necesaria. La razón es simple: es un tipo diferente de codificación y caeríamos en un error si asumiéramos que solo por nuestra experiencia previa, poseemos la habilidad innata para escribirlos.

Muchos de los tests unitarios que encontramos en quienes se inician en esta metodología son francamente inútiles. No es culpa de los desarrolladores; para la gran mayoría, el primer paso es instalar un framework del tipo NUnit y lanzarse a escribir tests previos para aquellas clases o métodos que necesitan. Una vez el ciclo en marcha, puede parecer que alternar las luces rojas y verdes es el buen camino: escribimos un test que falla, hacemos lo necesario para superarlo y avanzamos. ¿Todo correcto? No!

Si la calidad de nuestros tests no es la suficiente, entonces éstos no aportan ningún valor añadido, sino todo lo contrario.

Uso incorrecto de los tests unitarios

Es importante partir de que los tests unitarios son útiles, pero siempre cuando son utilizados de forma correcta dentro de un Desarrollo Dirigido por Tests (TDD). Por mi experiencia, compartida con otros desarrolladores, los tests no son efectivos, por ejemplo, para encontrar bugs en un código o para detectar un estado de regresión.

Por definición, un test unitario sirve para examinar cada unidad de un código de forma indepediente. Sin embargo, en un entorno real, todas estas unidades deben funcionar de una forma conjunta. En este caso, se aplica la máxima de que el todo es más que la suma de las partes: el hecho de que dos componentes, X e Y, posean sus respectivos test y estos se superen de forma independiente, no implica que necesariamente sean compatibles entre ellos.

Y ésto, es un problema inherente a todo test: cuando estamos diseñando las condiciones previas de un test unitario, no podemos detectar aquellos problemas derivados de la interacción cruzada de un elemento con otro(s). Este sería el caso por ejemplo de un módulo AJAX cuya respuesta es necesaria para la interpretación de un segundo. En este supuesto, debemos recurrir a elementos extraños como los mocks cuyos contras suelen pesar más que sus pros en el desarrollo de aplicaciones complejas.

Por lo tanto, si nuestro objetivo es encontrar bugs en un código, puede resultar más efectivo utilizar las técnicas convencionales, esto es, ejecutar una aplicación en un contexto de preproducción con todos sus elementos integrados y proceder de forma manual o automatizada. Antes de que se me echen encima los defensores del agilismo, la afirmación anterior es solo una invitación a crear un tipo de test automatizado para comprobar la integridad de una aplicación al que solemos referirnos como test de integración. En la actualidad existen diversas herramientas para este tipo de comprobación como, por ejemplo, Selenium para los navegadores web.

Una herramienta para cada propósito

La siguiente tabla refleja la herramienta más apropiada para cada tarea concreta:

Objetivo Técnica más apropiada
Encontrar errores (cosas que no funcionan según lo esperado) Tests Manual (Puede complementarse con tests de integración)
Detectar regresiones (cosas que solían funcionar pero que han dejado de hacerlo inesperadamente) Tests de Integración automatizados (Puede complementarse ocasionalmente con tests manuales)
Diseño de componentes de software robustos Tests unitarios dentro de una metodología TDD

 

NOTA: Hay sin embargo un caso en el que los tests unitarios pueden resultar útiles localizando bugs: durante el proceso de refactorización. En este supuesto, los tests pueden mostrar en qué paso se ha introducido el error ayudando a subsanarlo de un modo rápido y efectivo.

Uso correcto de los tests unitarios

Por lo tanto, tras lo visto anteriormente, podemos reafirmar que un test unitario resulta especialmente útil solo en el contexto de un Desarrollo Dirigido por Tests. Porque, como afirman los gurús de esta metodología, TDD es un proceso de diseño, no un proceso de testing.

Buenos tests vs malos tests

TDD facilita la creación de componentes de software que, individualmente, se comportan según un diseño. Un conjunto (suite) de buenos tests unitarios tienen un valor incalculable dentro de un proyecto: documentan de forma precisa nuestros diseños y hacen fácil tanto el refactorizado como su expansión mientras mantienen la claridad del conjunto. Sin embargo, una mala suite puede dificultar enormemente un desarrollo: no ofrecen claridad, no garantizan el funcionamiento correcto de un código en todo escenario posible y puede eliminar toda posibilidad de acometer un refactorizado con garantías.

En otras palabras, mientras que un buen uso de los tests garantizan una amplia cobertura del código sobre el que se aplican, un mal uso puede significar un falso (o incluso negativo) respaldo frente a eventualidades.

Pistas para escribir buenos tests unitarios

A continuación se proponen algunas puntos para aumentar la efectividad de nuestros tests:

  • Cada test debe ser independiente al resto. Cualquier comportamiento dado debe corresponderse con un único test. Esto es así para que, en caso de modificar una funcionalidad, solo un test se vea afectado. Esta regla posee algunos matices:

    No debemos realizar afirmaciones innecesarias. No es productivo realizar afirmaciones para algo que ya ha sido comprobado en otro test: con esto no ampliamos la cobertura de nuestras pruebas, sino que únicamente añadimos la frecuencia con la que nos será advertido un determinado error. En esta línea, existe una máxima (algo radical) que defiende una única afirmación logica por cada test unitario.

    Comprueba solo una unidad de código cada vez. Los tests, una vez más, deben ser independientes: deben centrarse en una única unidad de código. Si éstos se solapan, un cambio puede resultar en la necesidad de actualizar toda una batería en cascada.

    Solo si la arquitectura lo exige, recurriremos a los mocks. Cuando necesariamente el resultado de un test depende de la funcionalidad de otra unidad de código y esto es inalterable, usaremos un mock u objeto dummy. Este punto es delicado y muchos puristas defenderán que si una suite precisa de mocks es porque no está bien planteada. Este punto asume que no existe alternativa y, por tanto, el uso de estos objetos de sustitución está justificado.

    Evitar o reducir al máximo las condiciones previas a la ejecución de una suite. En la misma línea que los anteriores puntos, una batería de tests que precisa de preconfigurar un escenario con muchos parámetros dificulta la claridad del código. Además, alteran la naturaleza de los tests ya que, finalmente, no estamos trabajando sobre unidades de código sino sobre sistemas dependientes.
  • Nombra los tests de un modo claro, consistente y unívoco. Puede resultar obvio, pero en la práctica es frecuente perder la consistencia en cómo nombramos los tests. Debemos tener en cuenta que son la documentación más fiable de la que disponemos: como desarrolladores, no debemos fiarnos de especificaciones escritas, diagramas o wireframes; la única verdad sobre el funcionamiento de un software es la que podemos rastrear a través de su propio código. Los tests aportan una capa de información (además de una estructura lógica) valiosísima a dicha documentación. Un test unitario no debería presentar comentarios: su propio nombre debería ser lo suficientemente explícito como para que no quepan dudas sobre su área de influencia.

Conclusión

Sin lugar a dudas, los tests unitarios pueden mejorar significativamente la calidad de nuestros proyectos a la vez que aumentan nuestra productividad de forma indirecta. Sin embargo, no hay que caer en la trampa de que un código que presenta tests unitario es mejor que otro que no; ésto es sólo relativo a la calidad de la suite diseñada y al uso que hacemos de los mismos.

Como hemos visto más arriba, existe un tipo concreto de test para cada situación: los tests unitarios son realmente útiles dentro de una dinámica de Desarrollo Dirigido por Tests y, aún en este escenario, necesitan ser precisos para aportar valor.

Más:

{6} Comentarios.

  1. Carlos

    Me encanta la entrada.

    Pero me surgen ciertas dudas de que algunas de estas premisas (pistas) puedan ser llevadas a cabo en Javascript.

    Antes de continuar me gustaría decir que llevo poco tiempo desarrollando en js por lo que a lo mejor suelto alguna burrada. No me lo tengais muy en cuenta porfa.

    La pista que no he sido capaz de llevar a cabo es la de “Comprobar solo una unidad de código cada vez”

    Ejemplo:
    Uno desarrolla un objeto siguiendo el patrón javascript proxy. En un principio no te preocupas de si los métodos de tu objeto son privados o públicos. Haces tu test de unidad para una pequeña funcionalidad, y después la implementas. Más adelante cuando terminas de desarrollar el objeto y te pones a refactorizar el código, descubres que deseas que parte de tus métodos sean privados, y ahí surge el dilema. Si hago el método privado no puedo testearlo, o para hacerlo he de llamar a otros métodos públicos que utilizen ese método por lo que mis tests se solapan y compruebo más de una ‘unidad de código’ a la vez (cosa que odio me fastidia bastante la verdad).

    En java esto no es un problema por que puedo declarar los métodos como protected y hacer que los test estén en el mismo paquete por lo que tengo acceso a todos los métodos protected. Pero en javascript si no me equivoco no se puede hacer nada parecido. Verdad???

    • Carlos Benítez

      Es muy interesante tu comentario.

      Hay un par de cuestiones en este caso que habría que contemplar; la primera y más importante tiene relación con esta frase:

      Más adelante cuando terminas de desarrollar el objeto y te pones a refactorizar el código…

      Error! :), para ser puristas, el refactorizado es el último paso de cada ciclo, por lo que no debemos postponerlo hasta la conclusión del objeto. Si a cada test hemos afinado el código, no es necesario un refactorizado posterior. Sin embargo, como he comentado durante el post, hay veces que podemos retrasar este proceso pero, ojo, solo es justificado cuando el código empleado para pasar el tests, no puede optimizarse por el momento más de lo ya escrito.

      Si no obstante tratamos de refactorizar un objeto ya terminado que posee sus métodos públicos y privados, pues tenemos un problema. Como bien comentas, en Javascript sería necesario incluir la capa de tests dentro del propio objeto con el fin de que pueda participar de sus métodos o variables privadas. Esto resultaría en un esquema insostenible que terminaría añadiendo más complejidad que claridad al conjunto.

      Para estos casos, los frameworks de tests como Jasmine, incorporan herramientas que permiten ‘observar’ el comportamiento de métodos privados: son los llamados ‘espias’. Este artículo puede darte una idea de cómo manejarlos.

      En este otro artículo hablan sobre el mismo tema utilizando JUnit en lugar de Jasmine.

      Espero haberte puesto sobre la pista.
      Saludos!

  2. Carlos

    mmm

    Me parece muy interesante tu respuesta.

    La verdad es que suelo intentar afinar al máximo mis objetos desde el principio. Aún así siempre reviso el conjunto una vez he acabado de desarrollar las funcionalidades del mismo. Con frecuendica acabo sacando código en común a un método aparte o modifico la visibilidad de algún otro.
    Normalmente saco a un test a parte la funcionalidad de este nuevo método. Sin embargo supongo que tampoco es estrictamente necesario.

    Muchas gracias por la respuesta.

  3. Jesus

    Hola,

    Como siempre, excelente artículo :), enhorabuena, soy un asiduo de tu blog.

    Como programador javascript de Front-End, cuando me dispongo a realizar los test de mi codigo, siempre me encuentro con el mismo problema:

    Como realizar test unitarios de métodos o funciones que realizan cambios en el DOM? Podríamos utilizar todas las herramientas que nos brinda JQuery para comprobar si un elemento está donde tiene que estar o tiene las clases css o propiedades necesarias, pero, no resultarían demasiado pesados los test? No resultaría demasiados complicados?

    Por lo que, llegamos a la siguiente pregunta: para probar temas de GUI, sería útil realizar test unitarios, o mejor realizar test manuales ??

    Saludos

    • Carlos Benítez

      Tienes toda la razón. Para los tests de GUI, siempre suele ser más interesante los tests manuales o aquellos que se realizan de forma ‘desatendida’ con herramientas específicas como Selenium.

      Los tests unitarios son más para el desarrollo de algoritmos y funcionalidades. Como bien comentas, testear el DOM con este tipo de tests exige de mocks y otras técnicas más complejas que resultan muy poco prácticas y suelen provocar más errores que ayudas.

      Saludos!

  4. Mateo Aluerd

    Este artículo es algo antiguo, seguro que incluso hay detalles en los que Carlos habrá aprendido y que pueda matizar más o (por que no) incluso tal vez rectificar algún detalle (Hablo por hablar :P)

    Pero yo queria decir que en la actualidad tb existen otras alternativas de testeo de GUI’s. Yo personalmente uso CasperJS que es un framework de PhantomJS. Se programa en Javascript y con una semanita ya lo tienes dominado.

Deja un comentario

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