TDD en Javascript: V Parte

22 Feb 2011

En la cuarta entrega de esta introducción al TDD en Javascript, realizamos nuestro primer kata con éxito. Sin embargo, la definición del desafío era sencilla y no presentaba demasiados problemas para resolverla. En el mundo real, las aplicaciones suelen ser mucho más complejas y es entonces cuando nos encontramos con dificultades para diseñar y aplicar nuestros tests.

En el desarrollo de aplicaciones Javascript, más que quizá en otros lenguajes, se requiere de un sólido entorno de pruebas y de muchas comprobaciones. Además de esto, solemos recurrir a ciertos patrones de diseño que se han demostrado muy útiles en la construcción de código. Por ejemplo, utilizamos el patrón singleton, los módulos, el facade, etc… Sin embargo, con la experiencia, aprendemos que muchos de estos patrones no son especialmente fáciles de testear por su propia naturaleza, haciendo entonces completamente inútiles las pruebas.

NOTA: Para más información sobre los principales patrones de diseño en Javascript, podemos leer el fantástico libro gratuíto de Osmani, Essential Javascript & jQuery Design Patterns.

En esta entrega, vamos a echar un vistazo a dos estructuras muy utilizadas que son especialmente delicadas cuando diseñamos los tests y que, por tanto, deberíamos evitar o reajustar acorde a una filosofía TDD. Los ejemplos han sido extraídos del artículo de Ben Cherry, Writing Testable JavaScript.

Para realizar las pruebas, utilizaremos en esta ocasión QUnit por su flexibilidad y rápida puesta en marcha. Tenéis más información sobre este framework en QUnit, testeando nuestras aplicaciones Javascript.

Patrón Singleton

El patrón del singleton es uno de los más populares en Javascript. En esencia, este patrón puede ser implementado creando una clase con un método que crea a su vez una nueva instancia de la clase si no existe. Resulta útil y simple, pero puede ser difícil de testear ya que el estado de la clase se altera durante las pruebas. En lugar de implementar este patrón como un módulo (que es lo usual), los componemos como objetos y les asignamos una única instancia por defecto en el ámbito global.

Cosideremos el siguiente ejemplo de módulo que utiliza el patrón singleton:

var dataStore = (function() {
  var data = [];
  return {
    push: function (item) {
      data.push(item);
    },
    pop: function() {
      return data.pop();
    },
    length: function() {
      return data.length;
    }
  };
}());

Realicemos ahora alguna prueba utilizando QUnit. El código sería:

module("dataStore");
test("pop", function() {
  dataStore.push("foo");
  dataStore.push("bar")
  equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item");
}); 
 
test("length", function() {
  dataStore.push("foo");
  equal(dataStore.length(), 1, "adding 1 item makes the length 1");
});

Cuando ejecutamos el conjunto de pruebas, la segunda afirmación ( dataStore.length ) falla. No es sencillo encontrar la causa a simple vista pero ahora que sabemos que algo está mal, vemos que hemos modificado el estado del objeto dataStore en el test anterior, por lo que el siguiente llega ‘contaminado’. Podríamos solucionar esto devolviendo dataStore a su estado anterior manualmente, pero esto complicaría el código en la parte de los tests.

Una mejor opción es rediseñar el módulo de la siguiente forma:

function newDataStore() {
  var data = [];
  return {
    push: function (item) {
      data.push(item);
    },
    pop: function() {
      return data.pop();
    },
    length: function() {
      return data.length;
    }
  };
} 
 
var dataStore = newDataStore();

Los tests serían:

module("dataStore");
test("pop", function() {
  var dataStore = newDataStore();
  dataStore.push("foo");
  dataStore.push("bar")
  equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item");
}); 
 
test("length", function() {
  var dataStore = newDataStore();
  dataStore.push("foo");
  equal(dataStore.length(), 1, "adding 1 item makes the length 1");
});

El comportamiento de nuestro módulo no ha variado con respecto al anterior. Sin embargo, ahora permite realizar diversos bancos de pruebas sin que uno contamine los resultados del siguiente. Cada prueba es propietaria de su propia instancia por lo que no interferimos en el flujo: logramos así cumplir una de las reglas de los test unitarios que recuerda que los tests deben ser inocuos.

Evitar aislar métodos a través de closures

Otro patrón común es el recurrir a lo que Douglas Crockford denominó ‘miembros privados‘ en Javascript. El objetivo de esta técnica es mantener el ámbito global libre de referencias innecesarias a métodos que deberían ser privados. Sin embargo, el uso excesivo de este patrón puede hacer prácticamente imposible el testear nuestro código ya que los frameworks no tienen acceso a esos métodos ‘ocultos‘.

Tomemos como ejemplo el siguiente código:

function Templater() {
  function supplant(str, params) {
    for (var prop in params) {
      str.split("{" + prop +"}").join(params[prop]);
    }
    return str;
  } 
 
  var templates = {}; 
 
  this.defineTemplate = function(name, template) {
    templates[name] = template;
  }; 
 
  this.render = function(name, params) {
    if (typeof templates[name] !== "string") {
      throw "Template " + name + " not found!";
    } 
 
    return supplant(templates[name], params);
  };
}

El método fundamental de nuestro objeto Templater es supplant, pero no podemos acceder a él ya que permanece como privado dentro del constructor. Por lo tanto, una suite de pruebas como QUnit, no puede acceder a él para verificar su funcionamiento. Además, no podemos comprobar que nuestro método defineTemplate hace algo sin invocar primero a .render() en la plantilla. Una forma de solucionar esto podría ser simplemente añadir un método getTemplate(), pero entonces estaríamos agregando referencias al ámbito público únicamente para pasar las pruebas.

En un caso como el anterior, modificar el patrón puede que no tenga mayores inconvenientes, pero cuando construimos objetos complejos con muchos métodos privados, la cosa se hace inmanejable. La mejor solución en este caso es rediseñar nuestro patrón para que los tests tengan acceso a todos los métodos:

function Templater() {
  this._templates = {};
} 
 
Templater.prototype = {
  _supplant: function(str, params) {
    for (var prop in params) {
      str.split("{" + prop +"}").join(params[prop]);
    }
    return str;
  },
  render: function(name, params) {
    if (typeof this._templates[name] !== "string") {
      throw "Template " + name + " not found!";
    } 
 
    return this._supplant(this._templates[name], params);
  },
  defineTemplate: function(name, template) {
    this._templates[name] = template;
  }
};

Y aquí está el código necesario para ejecutar las pruebas en QUnit:

module("Templater");
test("_supplant", function() {
  var templater = new Templater();
  equal(templater._supplant("{foo}", {foo: "bar"}), "bar"))
  equal(templater._supplant("foo {bar}", {bar: "baz"}), "foo baz"));
}); 
 
test("defineTemplate", function() {
  var templater = new Templater();
  templater.defineTemplate("foo", "{foo}");
  equal(template._templates.foo, "{foo}");
}); 
 
test("render", function() {
  var templater = new Templater();
  templater.defineTemplate("hello", "hello {world}!");
  equal(templater.render("hello", {world: "internet"}), "hello internet!");
});

Con el rediseño, todos los métodos son ahora accesibles de forma aislada, por lo que podemos utilizar sin miedo nuestro framework y descubrir errores fácilmente.

Conclusión

En esta entrega hemos visto cómo escribir un Javascript fácil de testear. Es cierto que esta revisión se hace con el objetivo de crear los test después que el código y no al revés como hemos estado hablando durante los capítulos anteriores. Sin embargo, los patrones de diseño tienen mucha fuerza cuando desarrollamos aplicaciones y un problema del TDD es que a veces no sabemos cómo encajarlos dentro de las especificaciones. Conocer sus variantes optimizadas para aceptar pruebas, puede darnos una guía, o receta, a la hora de afrontar el diseño.

Más:

{8} Comentarios.

  1. David Zabala

    Muchas gracias por estos artículos Carlos!
    He aprendido cosas que ignoraba por completo

    • Carlos Benítez

      Gracias David;
      en breve continuaré la serie con otros katas y técnicas más avanzadas de TDD.

      Un saludo!

  2. Andrés Martinez

    ¿Pero dejar los métodos privados de forma pública en el último ejemplo no es un problema para interaccionar con la aplicación? No hay una forma de que cuando no se esté en testing estos sean privados de verdad? Me parece un verdadero problema si yo al usar por ejemplo jQuery y escribir en el editor de repente la funcionalidad de livecoding me propone tropecientos metodos que no sirven. Es la única manera? Un saludo.

    • Carlos Benítez

      No es la única manera, pero si una sencilla y fácil de comprender.

      Pronto publicaré algunos métodos más avanzados sobre el ‘revelado’ de métodos de un modo más dinámico, esto es: hacerlos públicos durante el desarrollo para permitir así a los tests interactuar con los mismos pero volviéndolos privados para su paso a producción.

      Saludos!

  3. Andrés Martinez

    Por cierto, hay un error en el ejemplo. Pusiste template en vez de templater :
    Template.prototype = {

  4. Andrés Martinez

    Creo que faltaron temas que vendrían muy bien comentar. Por ejemplo los stubs y los spies. Y tal vez si recomiendan algunos videos de gente abordando casos mas concretos aplicado a lo que cualquiera puede necesitar en su web, por ejemplo un panel de administración o cosas similares más cotidianas fuera de una calculadora, sería genial.

    • Carlos Benítez

      Si esta serie es algo antigua y han aparecido muchas bibliotecas y herramientas en Javascript que llevan el Desarrollo Dirigido por Tests mucho más allá.

      En breve publicaré una serie de artículos que completan esos aspectos.

      Saludos!

Deja un comentario

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