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 = {}; } Template.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.

Muchas gracias por estos artÃculos Carlos!
He aprendido cosas que ignoraba por completo
Gracias David;
en breve continuaré la serie con otros katas y técnicas más avanzadas de TDD.
Un saludo!