Introducción
No cabe duda de que el desarrollo de aplicaciones web es diferente al de las aplicaciones de escritorio. La mayor parte del tiempo nos vemos pulsando F5 para refrescar el navegador, mirando rápidamente el contenido y volviendo de inmediato al editor para continuar escribiendo código. Esta misma combinación la repetimos tantas veces a lo largo del día que la hemos convertido en algo reflejo, automático, como un parpadeo.
Pero podemos ir más allá: cuando programamos para la web, no basta con ejecutar nuestro código en un sólo navegador. Cada pieza que escribimos, tenemos que comprobarla en una amplia lista de dispositivos y platadormas para, la mayoría de las veces, terminar trabajándola de forma específica en cada una de ellas.
Finalmente, no podemos olvidar el tema de la optimización. Las aplicaciones web consumen ancho de banda y tienen que descargarse en el cliente antes de ejecutarse. En el caso del Javascript, tenemos incluso otra variable: el entorno del usuario final. Esto significa que es importante conseguir códigos muy eficientes con el menor tamaño posible. La clave de este proceso está en la refactorización a la que volveremos más adelante.
En Javascript, toda la tarea de refactorización y tests se hace aún más importante debido a la naturaleza no intrusiva de este lenguaje: Javascript fue diseñado para mejorar la experiencia de usuario por lo que cuando se produce un error, éste lo hace de forma silenciosa. Por si esto no fuera suficiente, la información que facilitan los navegadores es, por lo general, escasa, críptica y poco intuitiva (cuando no errónea como en el caso de IE).
TDD o Desarrollo Dirigido por Tests
El TDD es una técnica de programación que cede todo el protagonismo del desarrollo a los tests unitarios. Un test unitario es una pieza de código que comprueba el resultado de otro código. Funcionan tomando un objeto en un estado determinado y actúando con él (por ejemplo llamando a un método o una función) para comprobar que el valor obtenido se corresponde con el esperado. Algo del tipo:
«Si el resultado de la función ‘Foo’ es igual a 5, todo está correcto»
Esto quiere decir que en el TDD, el primer paso es escribir las especificaciones en forma de tests para luego continuar con el código impresicindible para superarlos.
No cabe duda que este cambio de prioridades resulta al principio chocante, pero finalmente, los resultados se traducen en una aplicaciones mucho más robustas, limpias y mantenibles.
Implementación
El TDD es un proceso cíclico de desarrollo (un algoritmo) en el que en cada iteración seguimos los siguientes pasos:
- Creamos un test a modo de especificación sobre el comportamiento de nuestra aplicación.
- Comprobamos que el test falla ya que aún no hemos implementación la especificación.
- Escribimos el mínimo código posible para superar el test anterior con éxito.
- Refactorizamos el código escrito para eliminar duplicidad y mejorar arquitectura.
- Comprobamos que no hemos alterado el comportamiento probando de nuevo los tests anteriores.
Veámos este proceso un poco más en detalle.
Creación del Test
Antes de escribir código, es necesario conocer el requisito a cumplir. Este requisito, o especificación, lo expresamos en forma de test sobre el que más tarde nos basaremos para programar. En Javascript, por lo general, un test se basa en una afirmación sobre un resultado obtenido. En la mayoría de los casos, necesitaremos comprobar que un valor devuelto por una función, método o evento, se corresponde con el esperado por el programa. Por lo tanto, hablamos de una estructura similar a:
Si (x) vale (y), entonces el test es correcto.
El valor de (x) será el dato dinámico que comparamos con el esperado (y). Para una mayor claridad, podemos reescribir la afirmación anterior así:
Si el valor que me devuelve mi código (x) es igual al que espero (y), entonces el código funciona.
La construcción del test es una de las partes más delicadas del proceso ya que es necesario que éste tenga sentido. A veces, es difícil escribir un test porque no tenemos realmente clara la funcionalidad de aquello que estamos programando. Este paso es por lo tanto importante en el ámbito del diseño: si no somos capaces de escribir los tests correctos, es posiblemente porque no tenemos completamente definida la finalidad de nuestra aplicación.
Implementar el código que supere el test
Una vez tenemos el test preparado, es necesario codificar lo mínimo necesario para que se supere con éxito. En este apartado, cuando hacemos hincapié en el mínimo código posible, es importante asumir que realmente se trata de escribir lo imprescindible. Por lo general, cuando atacamos un test, siempre terminamos codificando mucho más de lo necesario: es una deformación profesional. Comenzamos a ver más allá y complicamos el código desde el principio. Vamos a adelantarnos con un ejemplo para subrayar que mínimo, significa realmente lo justo.
Pensemos en una función con dos argumentos que calcula el área de un rectángulo ( base x altura ). El primer test de aceptación, sería comprobar el resultado a través de una operación básica:
[ TEST en pseudo código ]
5 x 4 = 20 ( base x altura = área )
Los valores de base y altura son aleatorios, y únicamente los utilizamos para componer la afirmación que nuestra función debe superar. Queda claro que los dos argumentos que recibe nuestra función son, por tanto, el valor de ‘base‘ y el valor de ‘altura‘ y que, operando con ellos, obtendríamos el área. Para superar este primer test, el mínimo código posible es, seguramente, más simple de lo que escribiríamos sin pensar demasiado:
[Código] function area( aBase, aHeight ){ return 20; } |
Es simple, poco funcional de momento, pero totalmente válido. Más adelante, cuando añadamos tests más precisos, modificaremos esta función refinándola.
Refactorización
Cuando hayamos escrito un cierto número de tests, es natural que aparezcan ‘malos olores’ en el código: duplicidad, arquitectura compleja, etc… por lo que será necesario pararse un momento sobre el resultado y ver si puede mejorarse. La refactorización, es la etapa de limpieza: cambiar la implementación manteniendo intacto el comportamiento. Esto quiere decir que tras cada mejora, los tests deben continuar superándose correctamente. En caso contrario, habremos modificado la funcionalidad y tendremos que volver al punto anterior para refactorizar de nuevo. Cuando abordemos ejemplos concretos de aplicación, retomaremos este apartado.
Beneficios directos del TDD
Tras un rápido vistazo a la teoría, tenemos que pensar qué ventajas podemos obtener si aplicamos TDD a nuestros proyectos. La metodología exige un esfuerzo considerable en cuanto al desarrollo tradicional; sin embargo, no utilizarla, significa tener que testear los procesos a mano una vez finalizados, lo que siempre es ineficiente. Recurrimos a la consola de Firebug una y otra vez, rellenamos decenas de veces un mismo formulario para comprobar un valor tras cada refresco, etc. Contar con una batería de tests automatizados permite escribir dichos tests una sola vez y ejecutarlos más adelante tantas veces como necesitemos.
Las ventajas más obvias de este sistema se pueden resumir en:
- Refactorización. Como hemos comentado antes, en Internet las aplicaciones tienen que ser lo más eficientes posible con la menor cantidad de código. Alcanzar este punto exige de refactorizar nuestro trabajo, una práctica que además mejora significativamente el diseño. Aplicamos así el paradigma DRY ( Don’t Repeat Yourself ) a la vez que conseguimos un mejor rendimiento mediante los patrones de diseño. Pero tenemos que andarnos con ojo ya que, cambiar toda una implementación sin alterar su comportamiento, puede provocar errores. En este aspecto, los tests son fundamentales para garantizar que, tras cada modificación, conservamos intacta la funcionalidad.
- Cross-Browser. Nos guste o no, existen un elevado número de plataformas y dispositivos en los que nuestra aplicación debe funcionar como se espera. Si optamos por realizar pruebas manuales, deberemos repetirlas en cada uno de los entornos para los que queremos ofrecer compatibilidad. Disponer de todos los tests necesarios incluso antes del código, simplifica la tarea.
- Diseño y abstracción. Personalmente, uno de los puntos más interesante de aplicar TDD es que exige conocer perfectamente la funcionalidad de una pieza de código antes de escribirla. Al comenzar por un test, estamos adelantado trabajo y tomando decisiones sobre el resultado: mentalmente nombramos los métodos que tendremos que implementar, las variables o funciones, pensamos qué errores pueden darse si el test fuera de otra manera, etc. En definitiva, el TDD nos exige un esfuerzo mental anterior a la escritura del código que nos da la justa medida tanto sobre lo que conocemos como sobre lo que no.
Posibles problemas o desventajas del TDD
Preparar las afirmaciones es complejo; muchas veces no estamos seguros sobre qué debemos probar exactamente y terminamos, finalmente, escribiendo malos tests. Para evitarlo, hay que recordar que siempre debemos comprobar la menor cantidad de funcionalidad posible. Y esto, requiere práctica: con el tiempo, cada vez conseguiremos afirmaciones más claras y certeras hasta que llegados a un punto, saldrán de forma expontánea y casi sin esfuerzo.
Hasta aquí, la introducción. En la segunda parte de esta serie, hablaremos sobre las herramientas específicas más populares para comenzar a aplicar TDD en Javascript.
Desde que leí tus artículos en ontuts me parecieron excelentes, y ahora leyendo tus artículos en tu blog me parecen sorprendentes en realidad muy buena calidad de información tienes y muchas gracias por compartirlas.
Desde ahora fiel seguidor a tus publicaciones. 😀
Gracias C0dex__!
La idea es trabajar duro para compartir conocimientos y aprender con la comunidad.
Y la motivación para ello es que amigos como tú encuentren el contenido interesante! 🙂
Saludos!
Soy nuevo en esto del TDD, llevo muchos años picando código y he pillado malos hábitos que ahora tengo que corregir. Gracias por el artículo, me a aclarado mucho las ideas en cuanto a la pilosofía de esta metodología.
un saludo!