Introducción al TDD (con JS) y porque no hay que evitarlo

Desde hace un tiempo vengo leyendo y probando algunas cosas de TDD con Javascript. Pero hace relativamente poco tiempo pude implementar de manera eficiente.
Algo que resulta poco menos que ‘natural’ en Java o incluso en PHP, en Javascript parecia un poco más complicado. ¿Por qué? Porque estaba trabajando mal.

La teoria es bastante simple y obviamente, es la misma que en cualquier lenguaje.
Escribimos un pedazo de código que haga uso de la unidad que queremos probar antes de escribir la implementación de dicha unidad, luego escribimos el código necesario para que dicha prueba sea exitosa y finalmente, refactorizamos para hacer nuestra implementación más general.

Veamos una pequeña introducción (si ya tenes idea de que va el TDD, pasa a la siguiente sección)

Introducción rápida al TDD

Un ejemplo clásico de TDD y unit testing es el de la calculadora. Veamos solamente la suma para hacerlo rápido:

 function test_sum() {
   var calculadora = new Calculadora();
   
   if (calculadora.suma(1, 3) === 4) {
     console.log("Test ok!");
   } else {
    console.log("Test fail!");
   }
 }

 test_sum();

Como podemos ver a simple vista, al ejecutar este extracto recibiremos un error diciendo “Calculadora is not defined” y eso esta bien. Es lo esperable, despues de todo, todavia no implementamos nuestro objeto Calculadora.

 function test_sum() {
   var calculadora = new Calculadora();
   
   if (calculadora.suma(1, 3) === 4) {
     console.log("Test ok!");
   } else {
    console.log("Test fail!");
   }
 }

 function Calculadora() {}

  Calculadora.prototype.suma = function() {
    return 4;
  }

 test_sum();

¡Hurra! ¡Nuestro test funciona! ya podemos empaquetar nuestra “Calculadora”, subirla a Github y esperar llenarnos de estrellas y hasta un post en Hackernews.
Bueno, no tan rápido. Nuestra calculadora, efectivamente, puede sumar 1+3, 2+2, -3+7 y cualquier otro par de digitos cuya suma sea igual a 4. Pero en cualquier otro caso, sabemos que va a fallar. Es ahí en donde vamos a refactorizar nuestro test para hacerlo más “inteligente” y de esta forma, mejorar nuestro código.

 function test_sum() {
   var calculadora = new Calculadora();
   
   var a = Math.floor(Math.random() * 10);
   var b = Math.floor(Math.random() * 10);
   
   if (calculadora.suma(a, b) === a + b) {
     console.log("Test ok!");
   } else {
    console.log("Test fail!");
   }
 }

 function Calculadora() {}

  Calculadora.prototype.suma = function() {
    return 4;
  }

 test_sum();

Cuando volvemos a ejecutar, el test fallará (salvo que tengas la mala buena suerte de que la suma de los números elegidos de forma aleatoria sea igual a 4. Así que vamos a darle los últimos retoques para que nuestro test se ejecute de forma correcta.

 function test_sum() {
   var calculadora = new Calculadora();
   
   var a = Math.floor(Math.random() * 10);
   var b = Math.floor(Math.Random() * 10);
   
   if (calculadora.suma(a, b) === a + b) {
     console.log("Test ok!");
   } else {
    console.log("Test fail!");
   }
 }

 function Calculadora() {}

  Calculadora.prototype.suma = function(a, b) {
    return a + b;
  }

 test_sum();

Ahora sí, nuestra calculadora puede sumar, sin importar que números le pasemos como parametro. O al menos, debería.

Obviamente, en js ya tenemos herramientas más robustas y maduras para realizar unit testing que un par de if’s.

O sea que Ahora tengo que perder mi tiempo escribiendo tests…

Este es quizás el pensamiento más común a la hora de empezar a escribir tests. El patrón EVO (Escribir y Ver que Onda) parece mucho más tentador y sencillo.
Sin embargo, utilizar TDD nos permite no solo ahorrar tiempo, si no que también nos aporta las siguientes ventajas:

  • Al escribir primero el test, vamos a tener en claro como utilizar nuestra líbreria/objeto/función por lo que reducimos tiempo en el diseño y en los eventuales “imprevistos” que podemos tener al “programar sobre la marcha”.
  • Si aparece un bug en nuestra aplicación, es mucho más fácil detectar de donde vino ya que podemos, por ejemplo, escribir tests que hagan uso de los valores que creemos que estan arrojando resultados incorrectos. Con esto podemos ver que nuestros tests pueden no ser perfectos, pero nos ayudaran a aislar el problema que tengamos.
  • Nos ayuda a pensar correctamente. A veces parece que olvidamos que los sistemas son Conjunto de partes interrelacionadas entre sí, aplicar TDD nos obliga a pensar en componentes y a tener en claro la entrada y la salida de cada uno de ellos llevando a disminuir el acoplamiento de los módulos que estemos desarrollando y la posibilidad de reutilizarlos.
  • Estamos forzados a simplificar nuestra aplicación. Divide y venceras.

Seguramente existen muchas otras ventajas, pero estas son las que pude rescatar en el tiempo que llevo usando TDD.

No puedo escribir Tests para lo que estoy haciendo, mís módulos dependen del DOM

Por alguna razón, el DOM nos asusta (Bueno, la razón es obvia, es horrible).

Sí, esta claro, nuestros módulos deben ser independientes del DOM. Sin embargo, tarde o temprano tenemos que hacer uso del mismo si queremos que el usuario vea algo de todo aquello que estuvimos escribiendo.

Por suerte, hoy en día proliferan los frameworks como Marionette que nos permite dividir nuestra aplicación y poder cumplir con los principios de TDD.
La mayoría de estos frameworks, solucionan muchos problemas comunes a la hora de desarrollar applicaciones y uno de ellos, es la posibilidad de pensar nuestras vistas como únidades átomicas y, por tanto, las hace ideales para TDD.

Por ejemplo, si utilizamos Marionette, podemos ‘renderizar’ nuestra vista, sin incluirla dentro de la estructura de nuestro sitio, pero aún así, hacer uso del DOM generado por la misma.

  var PersonaItemView = Marionette.ItemView.extend({
    template: "#persona"
  });

  function test_persona() {
    var personaMock = new Backbone.Model({
      nombre: "Pablo",
      edad: 26
    });
    personaItemView = new PersonaItemView({
      model: personaMock
    })

    personaItemView.render();

    if (personaItemView.$el.find('.name').text() === personaMock.get('nombre')) {
      console.log("Test ok!");
    } else {
      console.log("Test fail!");
    }
  }

Marionette nos permite acceder, mediante el atributo “$el” al elemento de jquery que genera nuestra vista. Permitiendo de esta forma poder probar su funcionamiento antes de insertarlo al documento real

Obviamente, podemos pensar esto de una forma agnóstica al framework. O bien adaptarlo al framework que estemos utilizando.
Podría darse el caso de que nuestra vista no posea un template. En esos casos, siempre podemos hacer un pequeño mock de la estructura real.
Si finalmente nos encontramos con que dicho mock resultaría terriblemente engorrosso o nos vemos en la necesidad de cargar un gran número de dependencias a fin de poder probar el módulo o unidad que estamos probando, entonces lo estamos haciendo mal.

Tener nuestra aplicación correctamente dividida, es escencial para poder mantenerla o incluso para poder desarrollarla. Javascript es particularmente propenso a crear código spagetti gracias a su versatilidad. Y sobre este punto hago la recomendación de utilizar un framework que nos fuerce a utilizar un patrón conocido o, al menos, una organización que nos pueda resultar facil de entender. No me interesa, particularmente, que SuperFramework2000 me permita hacer en dos líneas de código que mi navegador me cocine una milanesa Me interesa saber donde tengo que mirar si quiero cambiar mi aplicación para que la milanesa este cocinada al horno en lugar de frita.

Planear una aplicación “en papel” y apegarnos 100% a esos hermosos diagramas de relaciones y de flujo que garabateamos resulta muy útil, pero muchas veces nos abstrae a un nivel en el aue dejamos muchas cosas afuera. TDD nos ayuda a identificar rapidamente estos problemas.

¿Y cómo escribo Tests en JS?

Bueno, ya mostre lo de crear una función que imprima algo en consola. Pero todos queremos juguetes lindos.

Actualmente, podemos nombrar tres frameworks de testing populares: Qunit, Jasmine y Mocha.

En lo personal, prefiero los dos últimos ya que los considero los más completos. Como siempre, es una cuestión de gustos. Jasmine posee muchas herramientas integradas y lo considero una muy buena opción para comenzar. Mocha, por otra parte, depende de otras herramientas, lo cual lejos de ser una desventaja, permite hacer cosas muy interesantes utilizando cosas como Sinon.js.

Independientemente del framework que elijamos, es importante destacar el uso de herramientas como Phantomjs y SlimerJS, que nos permiten correr un motor web desde la consola, de forma que resulte muy fácil incorporar nuestra suite de tests a un proceso de construcción mediante herramientas como Grunt. De hecho, en el caso de Jasmine,Grunt posee un modulo contrib muy completo para manejarlo.

No es mi intención, con este árticulo, enseñar alguna de estas herramientas. Pero si estas interesado, te recomiendo comenzar con Jasmine, ya que al poseer todas las herramientas integradas, es más sencillo para comenzar.

Pongo, a modo de ejemplo, como seria el test para la aplicación de la calculadora que escribí en la introducción utilizando Jasmine:

  describe('La clase Calculadora', function() {
    it('Puede sumar 1 y 3', function() {
      expect(calculadora.sum(1, 3)).toBe(4);  
    });

    it('Puede sumar cualquier par de numeros', function() {
      var a = Math.random();
      var b = Math.random();

      expect(calculadora.sum(a, b)).toBe(a + b);
    });
  });

Por último, en FernetJS tienen una buena introducción al uso de Mocha con expectjs.

expect(me.volverAEscribir()).toBe(‘pronto’);