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’);

  • ssebastianj

    * Oh, justo ayer estaba leyendo ese artículo en FernetJS sobre Mocha (leo Mocha y me dan ganas de tomar un coffee)

    * Jaja, sería bueno que el browser cocine una milanesa pero en ¡1 línea de código!

    * Angel “Java” Lopez viene “jodiendo” con TDD hace bastante: http://msmvps.com/blogs/lopez/archive/tags/TDD/default.aspx

    Está bueno TDD. A veces a la mayoría nos agarra pachorra andar escribiendo tests (y encima buenos tests) y vamos a las papas de una jeje.

    Me gustó el artículo Pablín, seguramente me será útil más adelante.

    [ModoQuejoso]
    Che, veo que cambiaste el theme y ahora a los snippets de código se los ve muy grande y no se logra apreciar todo el código sin hacer hacer scrolling horizontal.
    [/ModoQuejoso]

  • leferreyra

    Despues tambien estan frameworks como AngularJS donde es muy facil hacer los mocks de los servicios, ya que usa Dependency Injection.. tambien viene con un framework para hacer End 2 End tests, usa la sintaxis de Jasmine y los corre con Testacular.. ;)

    Buen Articulo!

  • Pablo Terradillos

    Gracias por el comment!, respecto al estilo. Es algo que deberia trabajar. El theme que estoy usando no tiene ninguna personalización y en estos días ando un poco complicado para sentarme un rato y acomodarlo. Pero ya lo haremos :)

  • Pablo Terradillos

    Gracias!

    En el árticulo mi idea era hablar de una forma general, más allá que se use Marionette, Ember, Angular, etc.

    Comparto que la inyección de dependencias es algo muy útil para hacer UT y que ademas refuerza lo que decia de tener siempre presentes y bien definidas las
    entradas/salidas de nuestros modulos.

    Se que Angular y Ember traen cosas muy interesantes para facilitar el unit testing (y tantas otras cosas), mientras que en Marionette vas a depender de herramientas ‘externas’ o un poco de trabajo extra (Algo que depende el caso puede ser visto como una ventaja o desventaja. Tengo pensado escribir algo sobre esto :) )

    Comentario adicional: No es complicado implementar DI en Backbone/Marionette. Como dije, la flexibilidad del framework es una de sus principales caracteristicas.

    Si te interesa, podes ver la charla “Dependency injection for fun and profit” en la Backboneconf sobre el tema: http://www.youtube.com/watch?v=Lm05e5sJaE8