En esta cuarta entrega sobre el curso de TDD en Javascript, ha llegado el momento de aplicar lo visto hasta ahora (I Parte, II Parte y III Parte) con ejemplos.
Para ir haciéndonos con la metodología y ampliar un poco nuestro vocabulario sobre metodologías ágiles, tenemos que introducir un nuevo concepto: el coding dojo.
Qué es un coding dojo?
Básicamente un coding dojo es una reunión de programadores en la que el objetivo a cumplir es trabajar sobre un desafio planteado. La idea es poner en común nuestras habilidades y mejorarlas. El ejercicio se considera completo cuando se ha cumplido el objetivo y puede ser repetido por el resto de la comunidad.
Existen varios tipos o modalidades de Coding Dojo, pero nos centraremos en aquella que requiere del TDD para su consecución: la CodeKata o, para simplificar, kata a secas.
CodeKata
Si, es cierto: dojos, katas, parece que tengamos que echar mano de un kimono y ajustarnos el cinto. La terminología del agilismo a veces recuerda a la de las artes marciales. Y ciertamente, puede que sea una analogía muy acertada si revisamos su definición. Según la Wikipedia, un arte marcial,
Por tanto, llevado a la programación, podemos hablar de un conjunto de prácticas y patrones codificados cuyo objetivo es concluir una tarea de forma correcta mediante la técnica.
Las particularidades de un CodeKata son:
- Un participante mostrará la solución al desafío partiendo desde cero
- Debe usarse TDD durante el desarrollo
- Debe hacerse en pequeños pasos que tengan sentido en sí mismos para que la comunidad pueda más tarde estudiarlos y reproducirlos
Y ya tenemos planteado nuestro escenario: partimos de un reto que tendremos que resolver utilizando TDD y en pequeños pasos tan didácticos como nos sea posible para que puedan ser estudiados.
Eso haremos. Utilizaremos como framework para conducir nuestras pruebas la librería Jasmine. He optado por esta en lugar de cualquiera de las muchas existentes por la importante razón de que, indudablemente, está de moda. Es muy sencilla de utilizar, no requiere de otras dependencias para su funcionamiento y genera unos resultados HTML elegantemente codificados. Algunos de los proyectos punteros que utilizan Jasmine son EaselTV, Diaspora o el reciente fork de JSLint, JSHint.
El desafío
Para este primer ejemplo, utilizaremos un desafío sumamente simple para ir haciéndonos con la metodología poco a poco. Recurriremos a un ejericico clásico utilizado, entre otras cosas, para discriminar a los falsos programadores en algunas entrevistas de trabajo: el FizzBuzz kata.
La idea es la siguiente: el programa debe imprimir en pantalla (o en la consola) los primeros 100 números. Si uno de ellos resulta múltiplo de 3, se escribe en su lugar la palabra «Fizz»; si el número es múltiplo de 5, escribiremos «Buzz»; si el número es múltiplo tanto de 3 como de 5, escribimos «FizzBuzz». En el caso de cero, escribimos «zero».
Prepando Jasmine
Jasmine apenas requiere configuración. Únicamente descargamos su última versión desde su página oficial (aquí) y guardamos la carpeta en el raíz de nuestro proyecto. La estructura de directorios queda de la siguiente manera:
Editamos el archivo SpecRunner.html (o creamos uno nuevo) con el siguiente contenido:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Jasmine Test Runner</title> <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.1/jasmine.css"> <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine.js"></script> <script type="text/javascript" src="lib/jasmine-1.0.1/jasmine-html.js"></script> <!-- include source files here... --> <script type="text/javascript" src="src/fizzbuzz.js"></script> <!-- include spec files here... --> <script type="text/javascript" src="spec/fizzbuzzSpec.js"></script> </head> <body> <script type="text/javascript"> jasmine.getEnv().addReporter(new jasmine.TrivialReporter()); jasmine.getEnv().execute(); </script> </body> </html> |
Y ya lo tendremos listo para trabajar.
Necesitaremos ahora crear dos archivos JS, uno con el código que pretendemos evaluar y otro con los tests que efectuaremos.
El primero será fizzbuzz.js, dentro de la carpeta src, cuyo contenido de momento lo dejaremos vacío.
El segundo, fizzbuzzSpec.js, dentro de la carpeta spec, será en el que escribiremos los tests (especificaciones) que nuestra aplicación debe cumplir. Por ahora, su estructura será:
describe("FizzBuzz", function() { } ); |
Llegados a este punto, tenemos ya preparado todo lo necesario para comenzar a codificar nuestro kata.
Si cargamos el SpecRunner.html en nuestro navegador, obtendremos el siguiente resultado:
Esto quiere decir que, de momento, todo es correcto (verde). Comencemos a escribir código.
Como hemos visto en los capítulos anteriores, el primer paso de un Desarrollo Dirigido por Tests es, precisamente, escribir un test. Puede resultarnos raro el comenzar probando un código que aún no existe, por eso es resulta más intuitivo el término especificación y, como sabemos qué tiene que hacer nuestro programa, podemos ir elaborando dichas especificaciones.
Retomemos el enunciado: tendremos que generar un bucle que pinte los 100 primeros números y que cada número sea chequeado por una función o método con el fin de comprobar si se trata de alguno de los casos especiales que marca el desafío. El bucle, tendrá el siguiente aspecto:
for( var x = 0; x < 101; x++ ){ console.log( FizzBuzz.check( x ) ); } |
Nuestro objetivo entonces será escribir las especificaciones necesarias para el método check del objeto FizzBuzz. Comencemos por la primera: si el número es múltiplo de 3, escribiremos «Fizz» en su lugar.
Por lo tanto, la especificación es la siguiente:
it( 'should return Fizz for 3', function() { var result = FizzBuzz.check( 3 ); expect( result ).toEqual( 'Fizz' ); }); |
El nombre de la especificación resulta lo más descriptiva posible, y el ejemplo ha sido resumido al máximo: si al método check de la ‘clase’ FizzBuzz le pasamos como argumento 3, obviamente múltiplo de dicho número, el resultado debe ser igual (toEqual) a «Fizz».
Si nos paramos un momento a analizar lo que supone haber escrito este test, descubriremos algunas cuestiones interesantes. Para empezar, hemos nombrado la clase general para nuestro kata ‘FizzBuzz’ e incluso hemos definido uno de sus métodos: ‘check’. Es decir, aunque pareciera que no hemos hecho nada, en realidad hemos diseñado mentalmente gran parte del código.
Si recargamos nuestro HTML, el resultado será el esperado error en rojo:
1 spec, 1 failure |
Lo siguiente será escribir el mínimo código posible para que se cumpla dicha condición. Para ello, nos vamos al archivo correspondiente ( fizzbuzz.js ) y preparamos la clase y el método que ya hemos diseñado:
/** * FizzBuzz */ var FizzBuzz = function() { var checkValue = function( number ) { } return { check : checkValue } }(); |
Con esto, ya tenemos la estructura de nuestra clase y nuestro método. Sin embargo, no hemos aún atacado a la especificación: vamos a ello.
Como tanto hemos repetido, necesitamos el mínimo código posible por lo que, antes de que se nos vayan los dedos escribiendo más de la cuenta, nos ceñimos al guión:
if( number === 3 ) return 'Fizz'; |
Sencillo y simple. No tiene mayor funcionalidad, pero es lo mínimo necesario para superar el test. El código completo de fizzbuzz.js queda de momento así:
/** * FizzBuzz */ var FizzBuzz = function() { var checkValue = function( number ) { if( number === 3 ) return 'Fizz'; } return { check : checkValue } }(); |
Comprobamos que ocurre en nuestro fizzbuzz.html:
Verde: 1 spec, 0 failures |
No tenemos nada que refactorizar por el momento. Por lo que podemos ir por la siguiente especificación. Probemos con otro múltiplo de 3, por ejemplo, el 9.
it( 'should return Fizz for 9', function() { var result = FizzBuzz.check( 9 ); expect( result ).toEqual( 'Fizz' ); }); |
Comprobamos fizzbuzz.html de nuevo:
Rojo: 2 specs, 1 failure |
Escribimos el código mínimo para que la cosa marche en fizzbuzz.js:
if( number === 9 ) return 'Fizz'; |
De acuerdo; volvemos a tener verde, pero como podemos comprobar, es hora de realizar la primera refactorización. No tendría sentido el ir añadiendo a mano cada múltiplo de 3, por lo que buscamos la forma de abarcarlos a todos:
if( number % 3 === 0 ) return 'Fizz'; |
Tras el refactorizado por reducción, comprobamos nuestro html:
Verde: 2 spec, 0 failures |
Vamos avanzando, ¿verdad? La siguiente especificación es la correspondiente a los múltiplos de 5:
it( 'should return Fizz for 5', function() { var result = FizzBuzz.check( 5 ); expect( result ).toEqual( 'Buzz' ); }); |
Con ella, obtenemos de nuevo el resultado esperado:
Rojo: 3 specs, 1 failure |
Tratamos de cumplir la especificación como en el caso anterior:
if( number % 5 === 0 ) return 'Buzz'; |
Es cierto que ahora no es estrictamente el mínimo código necesario, pero como la dinámica es similar a la anterior, nos tomamos la licencia de avanzar algo más deprisa.
Verde: 3 specs, 0 failures |
Siguiente especificación: para los múltiplos tanto de 3 como de 5, escribimos «FizzBuzz»:
it( 'should return FizzBuzz for 15', function() { var result = FizzBuzz.check( 15 ); expect( result ).toEqual( 'FizzBuzz' ); }); |
En el html, obtenemos el resultado esperado:
Rojo: 4 specs, 1 failure |
Implementemos la solución. Si recurrimos a matemática básica, el mínimo común múltiplo de 3 y 5, es 15. Por lo tanto, la solución irá en esa dirección:
if( number % 15 === 0 ) return 'FizzBuzz'; |
Si ojeamos nuestro método hasta el momento, tenemos:
if( number % 3 === 0 ) return 'Fizz'; if( number % 5 === 0 ) return 'Buzz'; if( number % 15 === 0 ) return 'FizzBuzz'; |
Comprobamos nuestra aplicación:
Verde: 4 specs, 1 failures |
¿Qué ha pasado? Mmmm, claro: 15 es múltiplo de 3, por lo que se cumple el primer condicional y no llegamos al siguiente. Tenemos que tener en cuenta el orden de los condicionales:
if( number % 15 === 0 ) return 'FizzBuzz'; if( number % 3 === 0 ) return 'Fizz'; if( number % 5 === 0 ) return 'Buzz'; |
De este modo, si el número es múltiplo de 15, será el primer condicional el que actúe. Comprobamos de nuevo nuestro html:
4 specs, 0 failures |
Gracias a los tests, hemos detectado un error potencial y hemos sabido corregirlo; ya sólo por ello, merece la pena este proceso.
Última especificación: para el caso del cero, escribimos «zero».
it( 'should return Zero for 0', function() { var result = FizzBuzz.check( 0 ); expect( result ).toEqual( 'Zero' ); }); |
Y, lógicamente:
5 specs, 1 failure |
La implementación:
if( number === 0 ) return 'Zero'; |
Probamos:
Rojo: 5 specs, 1 failure |
Vaya, mismo error que antes; el módulo de cero es siempre cero, por lo que tenemos que poner nuestro último condicional el primero. Finalmente, nuestro método debería quedar así:
var checkValue = function( number ) { if( number === 0 ) return 'Zero'; if( number % 15 === 0 ) return 'FizzBuzz'; if( number % 3 === 0 ) return 'Fizz'; if( number % 5 === 0 ) return 'Buzz'; } |
Volvemos a probar:
Verde: 5 specs, 0 failures |
Y hemos concluido todas las especificaciones. A cada iteración de nuestro bucle inicial, comprobaremos los valores y devolveremos el resultado esperado.
Tendríamos finalmente que comprobar si podemos refactorizar una vez más. En este caso, como nuestra aplicación es muy sencilla, no es necesario. El código completo sería el siguiente:
/** * FizzBuzz */ var FizzBuzz = function() { var checkValue = function( number ) { if( number === 0 ) return 'Zero'; if( number % 15 === 0 ) return 'FizzBuzz'; if( number % 3 === 0 ) return 'Fizz'; if( number % 5 === 0 ) return 'Buzz'; } return { check : checkValue } }(); |
Conclusión
Hemos realizado con éxito nuestro primer kata con TDD. Es cierto que el desafío era muy simple, pero ha servido para poner en práctica todo lo que habíamos visto en las entregas anteriores. En el siguiente capítulo, afrontaremos un problema más complejo para el que necesitaremos exprimir nuestro framework un poco más.
Buenas,
En primer lugar agradecerte la entrada. Yo no estoy muy acostumbrado a hacer TDD, pero alguna vez que lo he hecho he intentado que los test cubrieran todos los casos posibles.
Quiero decir, comprobar que funciona con 3, 5 y 15, es algo que cuando estás programando, lo vas a chequear sí o sí. Los test yo los entiendo más como algo que debe de comprobar al máximo el comportamiento cuando se añadan nuevas funciones. Así, código y tests deben crecer y evolucionar a la par.
Dicho esto, qué pasa si el día de mañana, alguien añade en el método:
if( number % 45 === 0 ) return ‘Paquirrín’;
Si lanzamos el test seguirá yendo OK, si nadie se acuerda de que eso hubo algún día que tuvo que devolver otra cosa, la fastidiamos.
Así pues, yo haría algo de esto:
it( ‘should return Buzz for 5’, function() {
for(i=1;i<100;i++){
if(i % 5 === 0 && !(i%15 === 0)){
var result = FizzBuzz.check( i );
expect( result ).toEqual( 'Buzz' );
}else{
var result = FizzBuzz.check( i );
expect( result ).not.toEqual( 'Buzz' );
}
}
});
Problema con esto, que es bastante coñazo y al final terminas dejando de mantener los tests porque son más costosos que el código en sí.
Un saludo.
Es cierto que los tests, inicialmente son más costosos que el propio código. De hecho, hay una máxima muy recurrente que es: 5 minutos para escribir el código y 3 horas para escribir todos los tests que lo cubren. Pero un desarrollo responsable exige que toda implementación esté respaldada por tests con una cobertura mínima de entre un 90-95%.
Esto no garantiza un mejor código, ni la ausencia total de errores; como he mencionado muchas veces, es necesario que los tests sean buenos: malos tests solo significan ‘ruido’ que entorpecen más que ayudan. Sin embargo, si los tests son correctos, algo que se consigue con la práctica, la garantía de un código escalable sin errores es muy alta. Eso puede marcar la diferencia y ayudarnos a mejorar como desarrolladores. El tiempo de escritura de tests, nunca es tiempo perdido: es una inversión de futuro.
Saludos.