Curso de Testing Javascript Moderno. Parte 1: introducción y herramientas

16 Oct 2013

Introducción

Ha llovido mucho desde que publiqué aquella primera introducción al TDD en Javascript; desde entonces, el lenguaje en general y esta metodología en particular, han evolucionado significativamente: tenemos nuevas herramientas, nuevos entornos, nuevas prácticas… y, como no, hace falta actualizar todo lo que ya se escribió.

Durante los siguientes posts, vamos a repasar las metodologías y herramientas más modernas para testear código Javascript. He pensado el separar este minicurso en capítulos para evitar así una entrada gigantesca que luego prácticamente nadie acaba de leer. Así, cada uno puede escoger aquella sección que le resulte más interesante o simplemente aprovechar la división para hacer los comentarios o formular las dudas de una manera más localizada.

A modo de resumen general, repasaremos de modo extraordinariamente superficial qué es esto del testing (algo que realmente no ha cambiado) para pasar a describir cuáles son las herramientas que actualmente (finales de 2013) están más de moda. En los capítulos posteriores veremos algo de nomenclatura para mantener los estándares, cómo se configura nuestra plataforma (el software) para pruebas y, finalmente, todo lo relacionado con la estructura de tests aplicadas a código real, Ajax, fixtures, mocks, stubs, etc…

Todo esto tendrá miga, así que como dijo Lao-Tsé, todo gran viaje comienza con un primer paso. Vamos a ello.

Pruebas y testing: concepto

El objetivo de un entorno de pruebas es ofrecer una infraestructura de tests unitarios que respalden el código que está en desarrollo.

El paradigma de metodología test que que seguiremos será el Test Driven Development (TDD). No obstante, dado que estamos realizando una aplicación de arquitectura Javascript, recurriremos a un subconjunto del TDD como es el Behavior Driven Development (BDD).

TDD

Desarrollo guiado por pruebas de software, o Test-driven development (TDD) es una metodología que involucra dos prácticas: escribir las pruebas con anterioridad al código (Test First Development) y la refactorización (Refactoring) posterior del código producido.

Para escribir los tests, generalmente se utilizan las pruebas unitarias (unit test) siguiendo un estricto proceso: en primer lugar, se escribe una prueba cuya cobertura es la de evaluar pequeñas partes o funcionalidades a añadir verficando inmediatamente que éstas fallan. A continuación, se escribe el mínimo código necesario para superar el test y, finalmente, se tras recatoriza todo lo anterior asegurándose que las pruebas continúan superándose tal y como se espera.

El propósito del desarrollo guiado por pruebas es lograr un código limpio, preciso y estructurado que funciona. Para ello, los requisitos del software son traducidos a pequeñas pruebas -baby steps- que garantizan la funcionalidad completa que se pretende alcanzar.

BDD

El paradigma BDD puede entenderse como una metodología de segunda generación que aúna ideas del Domain Driven Development (DDD) con las ya referenciadas para el TDD. Además, a esta última se le suman diversos principios del diseño y análisis orientado a objetos con el fin de proporcionar tanto a desarrolladores de software como a los analistas de negocio una serie de herramientas y procesos compartidos con los que ambos pueden comunicarse fácilmente.

Ciclos de desarrollo

Dentro de una metodología TDD, el ciclo básico de desarrollo de software se puede dividir en tres fases:

  • Creación de un test que respalde el comportamiento a implementar.
  • Creación del mínimo código necesario para superar de forma positiva el test anterior.
  • Refactorizar el código escrito asegurándose que tanto su propio test como el resto, continúen resolviéndose satisfactoriamente.

Este paradigma implica por lo tanto que antes de añadir una nueva funcionalidad a nuestro software, debe escribirse siempre previamente un test. Por funcionalidad, entendemos también cualquier otro método, función o comportamiento nuevo en la aplicación. Dicho de otro modo: no deberíamos nunca escribir código ‘efectivo’ sin un test que lo respalde.

El objetivo de dicho test ‘prematuro‘ es acotar de forma precisa el alcance del código a desarrollar y realizar un primer ejercicio mental de análisis sobre el problema que se pretende resolver:

  • El acotar o delimitar una funcionalidad significa que no escribimos más código de la cuenta o implementamos funcionalidades que, pese a no estar en ese momento planteadas, presuponemos que pueden ser útiles en el futuro. Bajo el paradigma del TDD, esa inercia a escribir más código del estrictamente necesario en un momento dado ha de evitarse imperativamente.
  • Una primera toma de contacto sobre el código a implementar supone, entre otros, dar un primer nombre a nuestras funciones o métodos para añadirlas al test, pensar en sus parámetros, etc… Los tests, al actuar directamente sobre el código final, exigen de un esfuerzo de síntesis inicial que definen en gran medida lo que será el código posterior.

Configuración del entorno de pruebas: dependencias

Para la configuración del entorno de pruebas (tests) se utilizarán las siguientes dependencias:

  • Arquitectura por defecto de la aplicación: Backbone, Underscore, jQuery.
  • Ejecución de tests mediante línea de comandos (CLI): NodeJS.
  • Framework de pruebas unitarias: Mocha.
  • Biblioteca para afirmaciones: Chai.
  • Biblioteca para spies, stubs y mocks: SinonJS.
  • Entorno para la ejecución desatendida de tests: Test’em.

Como podéis comprobar, son muchas cosas, pero todas conocidas que seguro os suenan. Esta lista no quiere decir que únicamente hablaremos de estas herramientas y no otras igualmente importantes, pero si que serán aquellas sobre las que basaremos los ejemplos.

También comentar que aunque se haya escogido Backbone como framework MVVM por su gran aceptación por parte de la comunidad, las pruebas son válidas para cualquier otro entorno, ya sea Javascript estándar o un framework que hayamos desarrollado a medida. En el caso de aquellos que prefiráis por ejemplo Angular, hay plugins de estos módulos especialmente desarrollados para los entornos de pruebas, por lo que quizá podríais utilizar las nociones básicas que aquí veremos para aplicarlas después sobre estas herramientas concretas.

A continuación pasamos a describir rápidamente cada una de las bibliotecas que hemos mencionado más arriba:

MOCHA

Mocha es una plataforma, o framework, para la creación de tests unitarios que puede ejecutarse tanto en un entorno de navegador como a través de NodeJS. Su punto fuerte es la facilidad que proporciona para la ejecución de tests asíncronos en relación a otros frameworks similares como Jasmine o QUnit.

A efectos prácticos, durante nuestro desarrollo, Mocha nos ofrecerá un entorno en el que agrupar nuestros tests y mostrar los resultados de los mismos.

Cada bloque de tests se delimitará mediante un objeto de tipo ‘describe’ mientras que cada ‘caso’ a testear estará comprendido dentro de un objeto de tipo ‘it’. Un primer vistazo a esta estructura quedaría como sigue:

describe( "My first test block", function () {
    it( "testing something in our app", function () {
        // Test goes here...
    } );
 
    it( "testing another thing related in our app", function () {
        // Test goes here...
    } );
} );

CHAI

Para la realización de los tests, es necesaria un biblioteca que permita formular ‘preguntas’ al SUT (el método u objeto que estamos testeando). Es lo que en la terminología de tests se conoce como ‘assertions’.

Chai nos ofrece toda una colección de estas afirmaciones ya preparadas permitiéndonos además escoger entre diversas sintaxis a la hora de aplicarlas. Esto quiere decir que Chai incorpora tres fórmulas diferentes entre las que podremos optar según queramos seguir una forma clásica, una de segunda generación o un sistema más semántico (desde el punto de vista de la lengua inglesa).

Estas tres aproximaciones son:

Assert: la forma más clásica proveniente de la vieja escuela. Consiste en afirmar que un valor dado se corresponde con el esperado:

assert.typeOf( foo, 'string', 'foo is a string' );
assert.equal( foo, 'bar', 'foo equal `bar`' );
assert.lengthOf( foo, 3, 'foo`s value has a length of 3' );

Expect: segunda generación y, por lo tanto, más propia del BDD. Consiste en realizar las mismas afirmaciones anteriores pero utilizando un lenguaje natural y fluido. Como ventaja sobre el estilo anterior encontramos que las consultas pueden encadenarse (véase el último de los siguientes ejemplos):

expect( foo ).to.be.a( 'string' );
expect( foo ).to.equal( 'bar' );
expect( foo ).to.have.length( 3 );
expect( beverages ).to.have.property( 'tea' ).with.length( 3 );

Should: prácticamente idéntico al método anterior con la salvedad de que ‘extiende’ cada uno de los objetos a testear con un nuevo método (should) permitiendo así un lenguaje aún más natural:

foo.should.be.a( 'string' );
foo.should.equal( 'bar' );
foo.should.have.length( 3 );
beverages.should.have.property( 'tea' ).with.length( 3 );

La elección de uno u otro estilo responde más a nuestra experiencia o familiaridad con uno u otro que a criterios de rendimientos o calidad.

La recomendación que damos es la elección del segundo estilo (expect) en caso de indiferencia. El porqué de esta elección frente a las otras se puede resumir en los siguientes puntos:

  • La sintaxis clásica de Asserts no permite encadenar afirmaciones, obligando a escribir cada una como un comando separado pese a que pudieran pertenecer al mismo ‘objeto de test’.
  • La sintaxis should, al extender cada uno de los objetos con un nuevo método, puede provocar en errores en navegadores antiguos (IE) y en aquellos escenarios donde el objeto a testear puede no haber sido definido aún.

No obstante, durante los ejemplos de esta guía se usarán indistantemente los métodos segundo y tercero ya que presuponemos que no ofrecemos soporte a navegadores obsoletos.

SINON

Cuando se realizan tests, es frecuente tener que interactuar con la aplicación que estamos desarrollando hasta un punto en el que pueden verse comprometidos los datos reales que ésta maneja. Hablamos por ejemplo de un escenario donde se realicen consultas a una base de datos o el guardado, actualizado o borrado de registros en un sistema ya establecido. También puede darse el caso de precisemos por parte de la aplicación de una serie de consultas vía AJAX a un servidor al que no queremos acceder de forma real durante los tests, bien sea por seguridad, o bien porque directamente éste aún no exista.

Para trabajar en estos escenarios, tenemos a nuestra disposición una serie de elementos que se encargan de ‘emular’ a un posible servidor reemplazándolo, de darnos una respuesta fantasma controlada ante una determinada función o método, o simplemente de ‘espiar’ cada una de las llamadas a funciones que hacemos con el fin de interceptarlas. Es lo que en la jerga de tests conocemos como stubs, mocks y spies respectivamente.

Sinon nos ofrece el soporte para incorporar estos agentes a nuestros tests de un modo sencillo y flexible, permitiéndonos así ejecutar cualquier tipo de prueba sobre objetos, métodos, o incluso servidores, que aún no existen o que simplemente queremos mantener al margen de nuestros tests.

A modo introductorio, estos agentes pueden definirse como sigue:

  • Stubs: Se trata de objetos que durante los tests interceptan y anulan a otros métodos integrados en la lógica de la aplicación. Permiten de este modo alterar el flujo real del programa para adaptarlo a nuestra lógica de tests. Esta funcionalidad es especialmente interesante cuando se trabaja con bibliotecas de terceros o código aún por implementar. Uno de los casos de uso más frecuentes es el interceptar métodos o funciones que realizan acciones sobre una base de datos, evitando así llegar a escribir o alterar datos en ella de forma real: los test unitarios deben ser inocuos y no alterar en modo alguno a otros sistemas. Como característica propia de los stubs, indicar que éstos no tratan de imitar el código original que implementará el objeto del test, sino únicamente evitar el error que puede causar su indefinición.
  • Mocks: similar al stub en cuanto que intercepta una llamada a método o función alterando el flujo natural de la aplicación. La diferencia radica en que el mock imita el comportamiento ‘original’ del objeto del test proporcionando una respuesta similar a la que éste daría.
  • Spies: similar al anterior, los ‘espias’ pueden interceptar la llamada a una determinada función o método al tiempo que mantienen un ‘registro’ de las mismas. Esta funcionalidad es especialmente útil para comprobar que un determinado fragmento de código llama -o pasa- por una función o método concreto. El espía no interfiere en el resultado de la llamada, ni en los parámetros que se le envían, sino que únicamente se limita a registrar tanto ésta como el valor de retorno. Al contrario que con los stubs, el código sigue su flujo natural produciéndose un error en caso de que la función o método de destino no exista o directamente falle.

PLUGINS CHAI

Chai posee un interesante ecosistema de plugins que amplian sus funcionalidades nativas permitiendo con esto una mejor integración con otras herramientas. Aquí trabajaremos con dos de ellas:

  • Sinon Assertions for Chai: esta biblioteca actúa como una extensión de CHAI al extendernos sus métodos dentro del entorno de Sinon.
  • Chai Backbone: extensión que proporciona, como en el caso anterior, un conjunto de afirmaciones especifícias para su uso bajo una arquitectura Backbone.

TEST’EM

Para la ejecución desatendida de tests se recomienda utilizar esta herramienta basada en Node la cual monitoriza todos los ficheros involucrados y escucha los cambios que sobre ellos vamos realizando.

Básicamente se trata de un ‘watcher’ en segundo plano que se encarga de relanzar los tests por nosotros cuando cambiamos un fichero, ya sea donde escribimos las pruebas o bien donde éstas actúan.

Test’em proporciona una URL desde la que puede seguirse en un navegador todo el desarrollo de los tests respaldándose la información que ahí encontremos con la que va saliendo por el terminal.

Estrictamente, el uso de Test’em no es imprescindible para el desarrollo bajo tests pero si que se recomienda su uso para un seguimiento en tiempo real de los efectos de éstos sobre el código.

Conclusión

Y hasta aquí la primera entrega en la que hemos presentado lo que será este minicurso: tenemos ya el concepto y las herramientas. En la siguiente veremos algunos preparativos como la nomenclatura de ficheros que deberíamos adoptar y la puesta a punto del entorno, justo el paso previo a que empecemos a picar código y ver las consecuentes lucecitas rojas y verdes de los tests.

Nos leemos.

Más:

{4} Comentarios.

  1. Rubén

    Buen tutorial, espero ansioso las siguientes entregas.

  2. Héctor Zarco García

    Que ganas tenia de ver nuevos pots po aqui, mil gracias!!!!!

  3. Kalmet Join

    Excelente!!!

  4. Francisco

    Buena introducción a lo que es el testing y mejor estar al tanto de estas tecnologías y sus respectivas herramientas para trabajar bajo TDD o BDD.

Deja un comentario

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