Escribe mejores tests en Javascript

Hace unos días di un workshop en mi empresa sobre técnicas para escribir tests unitarios en Javascript. El objetivo del workshop era sentar las bases de lo que considero que son buenas prácticas en la escritura de tests.

Principios

Comencé por intentar establecer una serie de principios básicos que deben observarse siempre que se escribe un test. Es solo una selección de todos los que puedes leer en el repo de @crossrecursion js-unit-testing-guide.

1. Los nombres de los tests han de ser descriptivos.

El nombre de un test debe ser suficientemente transparente como para no dejar dudas sobre lo que testea. Ten en cuenta que el nombre del test se ha de leer de arriba a abajo; es decir, el nombre comienza en el describe más exterior y acaba en el it con la comprobación. En el ejemplo siguiente, el nombre del test es "Calculator, when at adding two natural numbers, should return a natural number".

describe( 'Calculator', function ()  
{
    describe( 'when adding two natural numbers', function ()
    {
        it( 'should return a natural number', function ()
        {

        } );
    } );
} );

Para que el texto que se genere se lea claramente, como en el ejemplo, es necesario seguir siempre la estructura siguiente:

describe( '[unit of work]', function ()  
{
    describe( 'when [scenario]', function ()
    {
        it( 'should [expected behaviour]', function ()
        {

        } );
    } );
} );

2. Testea una cosa a la vez.

Es fácil caer en la tentación de poner más de una afirmación (assertion) en una función it para comprobar un par de cosas que comparten mucha lógica. No lo hagas. En caso de errores, la salida de tus tests será mucho más clara y específica si testeas solo una cosa en cada test.

Aprovecha tu librería de tests y utiliza las funciones beforeEach y afterEach para poner ahí toda esa lógica compartida por varios it. Pero asegúrate de poner una sola assertion en cada test.

3. Testea el caso general y los casos extremos.

La gran duda que todos tenemos al hacer tests es qué debemos testear y qué no. Evidentemente, es IMPOSIBLE testearlo todo. Así que limítate a testear un único caso que sea representativo (=caso general) de cada escenario posible en tu código. Por ejemplo, si estás testeando la función de sumar de una calculadora, testea la suma 2 y 3, para el escenario de números naturales. Deberás testear, también, 2,3 y 5,4, para el escenario de números fraccionarios, y así sucesivamente, hasta agotar los escenarios posibles. El punto es testear solo 1 caso por cada escenario.

Por otra parte, debes testear aquellos casos que estás seguro que nunca se van a dar (=casos extremos). Siguiendo con el ejemplo anterior, podríamos testear la suma de 2 y NaN (not a number). Así, si se dan este tipo de casos, tendrás tu código preparado para lidiar con ello.

4. Usa factorías.

El uso de factorías se refiere a encapsular, dentro de una función, fragmentos de código que se repiten en varios tests. Así simplificamos la escritura de los tests y mejoramos su semántica. En el ejemplo siguiente se ha reutilizado la lógica de construcción de un objeto ProfileModule con la factoría createProfileModule. Como podrás ver, ganamos en reutilización de código y es más fácil de leer.

function createProfileModule( options )  
{
    return new ProfileModule( options || { views: 0 } );
}

describe( 'User profile module', function ()  
{
    it( 'should return the current views count', function ()
    {
        var profileModule = createProfileModule( { views: 3 } );
        expect( profileModule.getViewsCount() ).toBe( 3 );
    } );

    it( 'should increase the views count properly', function ()
    {
        var profileModule = createProfileModule( { views: 41 } );
        profileModule.incViewsCount();
        expect( profileModule.getViewsCount() ).toBe( 42 );
    } );
} );

5. Usa fake objects.

La librería SinonJS proporciona una serie de utilidades imprescindibles a la hora de testear tu código. Algunas de ellas son los fake objects, los cuales te permiten modificar el comportamiento de tu navegador para crear el entorno propicio en el que ejecutar tus tests. Los más importante son:

Fake timer

Es muy probable que tu código dependa en algún punto de que pase una cierta cantidad de tiempo antes de ejecutar otra parte de tu código. Aquí entramos en el problema de testear código asíncrono. Podríamos evitar en cierta medida este problema si lográramos que nuestro browser viaje al futuro de forma instantánea. De esta manera, no tendríamos que esperar nada para seguir ejecutando nuestro código. Fake timer nos permite cambiar el concepto de tiempo que tiene el browser, pudiendo adelantarlo (o retrasarlo) a voluntad.

{
    setUp: function () {
        this.clock = sinon.useFakeTimers();
    },

    tearDown: function () {
        this.clock.restore();
    },

    "test should animate element over 500ms" : function(){
        var el = jQuery("<div></div>");
        el.appendTo(document.body);

        el.animate({ height: "200px", width: "200px" });
        this.clock.tick(510);

        assertEquals("200px", el.css("height"));
        assertEquals("200px", el.css("width"));
    }
}

En el ejemplo adelantamos 500 ms en el tiempo para no tener que esperar a que acabe la animación y poder continuar con los tests.

Fake AJAX

Hoy en día no hay página que no tenga que hacer varias llamadas AJAX a algún servicio externo. Lo complicado de testear el código relacionado con estas llamadas es que no podemos depender de ningún servicio para poder ejecutar nuestros tests. Para ello, el "fake AJAX object" nos permite simular que ya hemos recibido respuesta de un servidor, aunque nunca se haya realizado la llamada. Por otra parte, podemos testear varios escenarios, pues podemos controlar el código de respuesta o el cuerpo de la respuesta.

{
    setUp: function () {
        this.xhr = sinon.useFakeXMLHttpRequest();
        var requests = this.requests = [];

        this.xhr.onCreate = function (xhr) {
            requests.push(xhr);
        };
    },

    tearDown: function () {
        this.xhr.restore();
    },

    "test should fetch comments from server" : function () {
        var callback = sinon.spy();
        myLib.getCommentsFor("/some/article", callback);
        assertEquals(1, this.requests.length);

        this.requests[0].respond(200, { "Content-Type": "application/json" },
                                 '[{ "id": 12, "comment": "Hey there" }]');
        assert(callback.calledWith([{ id: 12, comment: "Hey there" }]));
    }
}

Como puede verse en el ejemplo, es en la función this.requests[0].respond donde puedes setear la respuesta del servidor para la primera llamada AJAX que ocurra en tu código. Úsala para crear distintos escenarios y asegurarte de que puedes controlar tanto un caso de éxito como un error del servidor.

Fake server

La idea del fake server es realmente la misma que la del AJAX, pero suelo preferirla simplemente porque obtienes el mismo resultado con menos líneas de código.

{
    setUp: function () {
        this.server = sinon.fakeServer.create();
    },

    tearDown: function () {
        this.server.restore();
    },

    "test should fetch comments from server" : function () {
        this.server.respondWith("GET", "/some/article/comments.json",
            [200, { "Content-Type": "application/json" },
             '[{ "id": 12, "comment": "Hey there" }]']);

        var callback = sinon.spy();
        myLib.getCommentsFor("/some/article", callback);
        this.server.respond();

        sinon.assert.calledWith(callback, [{ id: 12, comment: "Hey there" }]);
    }
}

La magia ocurre en this.server.respondWith(), donde podemos indicar todos los detalles de la supuesta respuesta. No te olvides de llamar a this.server.respond() para que todo acabe funcionando.

Bonus

En el repositorio del workshop podrás encontrar la carpeta workshop. Una vez instaladas las dependencias con npm install, deberías poder correr los test con npm run test:watch.

Presta atención al código de cada fichero de test, pues en la cabecera hay un par de puntos críticos que quiero resaltar.

Con esto dejo el blog hasta septiembre! ... Espero que disfrutéis de las vacaciones.

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Suscríbete a mi lista de correo

* Campos obligatorios