Como hacer Unit Test en JavaScript con Mocha y Chai.

Desarrollo Web

Realizar Unit Test siguiendo el enfoque TDD es cada vez es más importante, sobre todo en aplicaciones modernas basadas en JS. Hoy aprenderás como hacer pruebas unitarias usando estas herramientas.

¿Qué es Unit Test y TDD?

La traducción al español de Unit Test es Pruebas Unitarias. Estás se enfocan en hacer pruebas a código para, mediante assets, comprobar si el funcionamiento es el que deseamos. Estás pruebas son escritas por los programadores y rara vez por las personas que hacen el Testing. Cabe destacar que no reemplaza el trabajo de un Tester, es solo una herramienta más y va más enfocada para programadores, para que al realizar cambios al código se pueda verificar que no estemos dañando otras partes del código que ya funcionan correctamente, además, es una guía para saber en que casos está fallando lo qué estamos "codeando".

TDD significa Test Driven Development (Desarrollo Dirigido por Pruebas), lo cual quiere decir que primero escribimos las pruebas y después el código, por lo que las pruebas nos irán orientando y nos ayudarán a escribir código de una manera más rápida.

Mocha y Chai

Chai es un framework para realizar pruebas a código mediante Asserts, Expects y Shoulds acoplándose muy bien a TDD y BDD. En su lugar Mocha es el ambiente, una herramienta donde podemos hacer nuestras pruebas mientras que Chai, es el framework con el que hacemos las pruebas.

¿Como funciona?

Es realmente sencillo entender el principio de TDD. Primeramente, escribimos la prueba, con todas las validaciones de todos los casos posibles. Para la prueba es importante que consideremos todos los posibles casos y eventos, no olvidemos incluir los casos que pudieran parecer sencillos o pudieran ser "obvios".

Configurar nuestro ambiente

Creamos una carpeta donde estará el módulo, la llamaremos slugify. Dentro de esta instanciamos nuestro módulo:

npm init -y

Nos creará el archivo package.json, del cual modificaremos 3 líneas, description agregando Unit Test, main agregando el archivo principal de entrada, en mi caso ./lib/index.js y finalmente author, con su nombre, quedando de la siguiente manera:

{
  "name": "slugify",
  "version": "1.0.0",
  "description": "Unit Test",
  "main": "./lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Luis Guillén",
  "license": "ISC"
}

Instalamos las dependencias, chai como dependencia de desarrollo y mocha como dependencia global

npm install chai --save-dev
npm install -g mocha

Al final nuestro archivo json, ya con las dependencias instaladas, quedará más o menos (cambiando solo la versión de chai) de la siguiente forma:

{
  "name": "slugify",
  "version": "1.0.0",
  "description": "Unit Test",
  "main": "./lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Luis Guillen",
  "license": "ISC",
  "devDependencies": {
    "chai": "^4.1.2"
  }
}

Ahora, solo queda crear nuestra prueba, para después, escribir el código. Existen 2 maneras de probar nuestro código, una es usando el navegador y la otra usando la terminal. Aquí solo veremos como hacerlas en la terminal.

La estructura que yo he definido es la siguiente:

slugify
    lib
    test
    package.json
prueba.js

Del cual, las carpetas tendrían la siguiente definición:

  • lib: como comentamos anteriormente, es donde estará el core de nuestro módulo.
  • test: donde pondremos nuestras pruebas.
  • package.json: archivo json.
  • prueba.js: este archivo, fuera de la carpeta del módulo, solo haremos pruebas manuales (pruebas no automatizadas, es decir, no usando unit test).

Crear la prueba

Vamos a hacer un función que convierta una frase a un selector css de tipo id o tipo class, es decir, que convierta "hola" a un selector id y sería "#hola" o bien a un selector class ".hola". Debemos considerar que, al ser una frase, su longitud no sea mayor a 100 caracteres y que el resultado no contenga ñ ni vocales con tilde.

Las pruebas son localizadas en la carpeta /test, aunque puedes ponerlos donde más te agrade. Veamos el código:

describe('Check if a phrase is Slugified', function(){
    //Test my code
    it('simple selector', function(){
        //hello
        //#hello .hello

        //container
        //#container .container

        //avenged sevenfold
        //#avenged-sevenfold .avenged-sevenfold

        //large space.
        //#large-space .large-space

    });

    it('selector with accent mark and ñ', function(){
        //año nuevo
        //#ao-nuevo .ao-nuevo

        //canción del corazón
        //#cancin-del-corazn .cancin-del-corazn

        //qué más da cómo estés.
        //#qu-ms-da-cmo-ests .qu-ms-da-cmo-ests

    });

    it('very long selector with accent mark and ñ and other non words characters', function(){
        //qué más da cómo estés (no, no estoy loco) amada mía.
        //#qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma .qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma

        //más vale que - entendeís que si - lo hagamos ya, sin remordimiento
        //#ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento .ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento

        //vale la pena no estár cegado por un amor al que no le importamos, pues ya que, vale estár enamorado pero no ciego, no engañeis a tu alma con una mentira.
        //error de longitud
    });
});

En el código anterior vemos dos funciones principales que aceptan una string como primer parámetro y un callback como segundo parámetro. También un conjunto de comentarios, donde hay una línea con la frase a convertir y finalmente otra línea con los selectores css e id. Las funciones son:

  • describe: Es donde englobamos nuestro conjunto de pruebas. Normalmente se usa para englobar las pruebas de una función del módulo a probar.
  • it: creamos una o varias pruebas a ciertos casos específicos de la función del módulo.

Ahora, vamos a crear las pruebas con las función de tipo assert. Chai nos provee de un conjunto de asserts que podemos ver en su página web y que podemos usar para hacer nuestras validaciones, aquí veremos algunas:

  • assert(expression, string): La función en si recibe dos parámetros, el primero una expresión de validación y como segundo parámetro una cadena que, en caso de rechazarse la validación, regresará el mensaje con ese texto.
  • assert.equal(actual, expected, [message]): hace la comparación entre el parámetro actual y expected, en caso de no ser iguales, manda el error Message (parámetro opcional).
  • assert.notEqual(actual, expected, [message]): hace la comparación entre el parámetro actual y expected, en caso de no cumplirse la desigualdad, manda el error Message (parámetro opcional).
  • assert.isTrue(value, [message]): compara si value es true.
  • assert.exists(value, [message]): revisa si value es null o undefined.
  • assert.throws(fn, [message]): compara si la función retorna una excepción con el texto message.

Finalmente, creemos todos los asserts necesarios para que se cumplan estás pruebas. En todos los casos solo queremos comparar si la función regresa una cadena determinada, por lo que usaremos assert.equal, solo en el último caso donde la cadena es más larga a los 100 caracteres, usaremos assert.throws.

var chai = require('chai');
var assert = chai.assert;
var slug = require('../lib/index.js');

describe('Check if a phrase is Slugified', function(){
    //Test my code
    it('simple selector', function(){
        //hello
        //#hello .hello
        assert.equal(slug.slugify('hello', '.'), '.hello');
        assert.equal(slug.slugify('hello', '#'), '#hello');

        //container
        //#container .container
        assert.equal(slug.slugify('container', '.'), '.container');
        assert.equal(slug.slugify('container', '#'), '#container');

        //avenged sevenfold
        //#avenged-sevenfold .avenged-sevenfold
        assert.equal(slug.slugify('avenged sevenfold', '.'), '.avenged-sevenfold');
        assert.equal(slug.slugify('avenged sevenfold', '#'), '#avenged-sevenfold');

        //large space.
        //#large-space .large-space
        assert.equal(slug.slugify('large space.', '.'), '.large-space');
        assert.equal(slug.slugify('large space.', '#'), '#large-space');
    });

    it('selector with accent mark and ñ', function(){
        //año nuevo
        //#ao-nuevo .ao-nuevo
        assert.equal(slug.slugify('año nuevo', '.'), '.ao-nuevo');
        assert.equal(slug.slugify('año nuevo', '#'), '#ao-nuevo');

        //canción del corazón
        //#cancin-del-corazn .cancin-del-corazn
        assert.equal(slug.slugify('canción del corazón', '.'), '.cancin-del-corazn');
        assert.equal(slug.slugify('canción del corazón', '#'), '#cancin-del-corazn');

        //qué más da cómo estés.
        //#qu-ms-da-cmo-ests .qu-ms-da-cmo-ests
        assert.equal(slug.slugify('qué más da cómo estés.', '.'), '.qu-ms-da-cmo-ests');
        assert.equal(slug.slugify('qué más da cómo estés.', '#'), '#qu-ms-da-cmo-ests');
    });

    it('very long selector with accent mark and ñ and other non words characters', function(){
        //qué más da cómo estés (no, no estoy loco) amada mía.
        //#qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma .qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma
        assert.equal(slug.slugify('qué más da cómo estés (no, no estoy loco) amada mía.', '.'), '.qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma');
        assert.equal(slug.slugify('qué más da cómo estés (no, no estoy loco) amada mía.', '#'), '#qu-ms-da-cmo-ests-no-no-estoy-loco-amada-ma');

        //más vale que - entendeís que si - lo hagamos ya, sin remordimiento
        //#ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento .ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento
        assert.equal(slug.slugify('más vale que - entendeís que si - lo hagamos ya, sin remordimiento', '.'), '.ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento');
        assert.equal(slug.slugify('más vale que - entendeís que si - lo hagamos ya, sin remordimiento', '#'), '#ms-vale-que-entendes-que-si-lo-hagamos-ya-sin-remordimiento');

        //vale la pena no estár cegado por un amor al que no le importamos, pues ya que, vale estár enamorado pero no ciego, no engañeis a tu alma con una mentira.
        //error de longitud
        assert.throws(function() {
            slug.slugify('vale la pena no estár cegado por un amor al que no le importamos, pues ya que, vale estár enamorado pero no ciego, no engañeis a tu alma con una mentira.', '.')
        }, 'text can not be gratter than 100 characters');
    });
});

Fijémonos en la parte superior donde importamos dos librerías, la línea 1 con chai, el framework de testing y en la tercera línea nuestro módulo a testear. Finalmente, solo tenemos que ejecutar la prueba.

Testing en la terminal

Para hacer el testing solo abrimos nuestra terminal y ejecutamos

c:/path/from/your/module> mocha

En primera instancia tendremos un error ya que no hemos creado nuestro módulo.

Creación del módulo slugify

Creamos un nuevo archivo dentro de la carpeta lib llamado index.js (recordemos que este archivo es el que definimos en el package.json en el main) y creamos nuestro módulo. Vamos a hacer uso de una función que ya fue escrita por alguien de la comunidad en esta página de github, copiamos y pegamos.

module.exports = {
    slugify: function(text, selector)
    {
            if(typeof selector != 'string')
                throw 'selector is not a string';

            if(typeof text != 'string')
                throw 'text is not a string';
            
            if(text.length > 100)
                throw 'text can not be gratter than 100 characters';
            
            return selector + text.toString().toLowerCase()
                .replace(/\s+/g, '-')           // Replace spaces with -
                .replace(/[^\w\-]+/g, '')       // Remove all non-word chars
                .replace(/\-\-+/g, '-')         // Replace multiple - with single -
                .replace(/^-+/, '')             // Trim - from start of text
                .replace(/-+$/, '');            // Trim - from end of text
    }
}

En el código anterior pegamos la función de la página mencionada con la estructura para crear módulos en JavaScript.

Corremos nuestra prueba y si todo está bien nos mostrará la siguiente imagen:

Unit test

Así, vemos que nuestro módulo ha finalizado con éxito.

Conclusión

Las pruebas unitarias nos ayudan a trabajar de una manera más rápida y con un panorama más amplio de lo que estamos haciendo, ya que nos van indicando, en caso de algún error, en que casos estamos fallando. Al inicio es un poco complicado poder seguir este patrón, pero una vez le tomamos el gusto, tiende a ser cada vez más sencillo y más práctico.