12Abr

Simulación de las peticiones AJAX con objectos simulados (mocks) gracias a Sinon bajo el framework Jasmine

El framework de pruebas Jasmine nos permite realizar las pruebas unitarias de nuestro código Javascript directamente en nuestro navegador o en un «headless browser» como PhantomJS para automatizar el proceso de pruebas…

El framework de pruebas Jasmine nos permite realizar las pruebas unitarias de nuestro código Javascript directamente en nuestro navegador o en un «headless browser» como PhantomJS para automatizar el proceso de pruebas.

En este artículo voy a presentar en unos pasos sencillos, como preparar el código javascript de nuestra página web para poder cubrir los casos de uso de forma unitaria.

Creando el archivo web

Teniendo el siguiente código HTML y Javascript en una sección de nuestra página web

index.html

<html>
    <head>
        <title>Jasmine and Sinon sample</title>
        <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
        <script src="news_reader.js"></script>
    </head>
    <body>
        <ul id="newsList"></ul>

        <script type="text/javascript">
            $(document).ready(function() {
                newsReader.read();
            });
        </script>
    </body>
</html>

news_reader.js

newsReader = {

    appendItem: function(item) {
        $("ul#newsList").append('<li><a href="'+item.link+'">'+item.title+'</a></li>');
    },

    read: function() {
        var self = this;
        $.ajax({
            url: "/latest-news",
            success: function(data) {
                data.forEach(function(newsItem) {
                    self.appendItem(newsItem);
                });
            }
        });
    }

};

Objetos simulados

Los objetos simulados (mocks en inglés) nos permiten tener componentes que imitan el comportamiento de objetos dentro del programa. No es necesario disponer del objeto real si
disponemos de un objeto que se comporta igual y tiene la misma interfaz.

La página HTML, contiene una lista que esperamos llenar con las últimas noticias a través de una llamada AJAX a nuestro servidor pidiendo las últimas noticias. Si queremos probar este componente, podemos querer hacerlo a través de una llamada real a nuestro servidor, pero si lo queremos hacer de forma unitaria no deberíamos usar ninguna llamada externa para poder comprobar el funcionamiento del objeto newsReader.

Para este cometido, usaremos Sinon JS que nos permite hacer un stub de los objetos de jQuery para poder simular la llamada AJAX al servidor. La librería de Javascript Sinon nos permite hacer objetos simulados (mocks) y observar métodos de objetos reales del código para poder
comprobar el comportamiento y comunicación entre los objetos, simulados o no.

Preparamos un archivo para ejecutar las pruebas con Jasmine. Debemos descargar la versión deseada desde Jasmine y añadir la libreria a nuestro path. El archivo de test inicial quedaría como sigue:

<html>
    <head>
        <title>Jasmine Unit testing with Sinon</title>

        <!-- Dependencias para Jasmine -->
        <link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-2.2.0/jasmine_favicon.png">
        <link rel="stylesheet" href="jasmine/lib/jasmine-2.2.0/jasmine.css">

        <script src="jasmine/lib/jasmine-2.2.0/jasmine.js"></script>
        <script src="jasmine/lib/jasmine-2.2.0/jasmine-html.js"></script>
        <script src="jasmine/lib/jasmine-2.2.0/boot.js"></script>

        <!-- Dependencies de nuestros tests -->
        <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
        <script src="sinon.js"></script>
        <script src="news_reader.js"></script>

        <!-- Nuestros tests -->
        <script src="specs.js"></script>

    </head>
    <body>
    </body>
</html>

Si descargáis de Jasmine un paquete del listado de releases en Github, podéis usar el fichero SpecRunner.html del raíz como guía para montar el archivo de pruebas.

En nuestro ejemplo estamos usando la versión de Jasmine 2.2.0. Os dejo un enlace directo a la descarga de la librería Jasmine en esa versión: jasmine-standalone-2.2.0.zip

A continuación tenemos el archivo spec.js con la prueba unitaria para comprobar el objeto de javascript newsReader:

spec.js

describe("news reader check", function() {

    it("method::appendItem adds an item to the UL#newsList when received a news item", function(done) {

        //El código del test aqui

        done();
    });

    it("method::read must call N times for a server response of N news item to appendItem method", function(done) {

        //El código del test aqui

        done();
    });

});

No hemos escrito aún código para las pruebas, si ejecutamos la vista de Jasmine, veremos el siguiente resultado:

Jasmine se queja de que no hay ninguna comprobación en esos tests, ya que están vacíos. Por un lado hemos escrito un test del método appendItem del objeto, para comprobar si cuando recibe un newsItem, lo añade al DOM correctamente. Y por otro lado hemos escrito un test que cuando recibe del servidor un array de noticias, esperamos que llame N veces al método appendItem.

Podemos tener cubierto el código de una forma básica con la ejecución de estos test. Añadimos el código javascript para testear el método appendItem:

...
it("method::appendItem adds an item to the UL#newsList when received a news item", function(done) {

    //Añadiendo un contenedor como el de nuestra vista
    $("body").append('<div id="playground"><ul id="newsList"></ul></div>');

    var sampleItem = {
        title: 'Docker Installation',
        link: 'http://docs.docker.com/installation/'
    }

    newsReader.appendItem(sampleItem);

    //Esperamos que se haya añadido el elemento a la lista
    expect($("ul#newsList").size()).toBe(1);
    expect($("ul#newsList li a").first().attr("href")).toBe("http://docs.docker.com/installation/");
    expect($("ul#newsList li a").first().text()).toBe("Docker Installation");

    //Limpiamos
    $("#playground").remove();

    done();
});
...

Ahora, cuando ejecutamos los tests, vemos como el primer test, pasa correctamente.

Simulando la llamada AJAX

Aunque no dispongamos del servicio real al cual hemos de llamar para obtener los elementos o si queremos aislar totalmente nuestras pruebas unitarias, podemos simular esa llamada reemplazando el método de jQuery, ajax por uno simulado. Con Sinon, este paso es bastante sencillo de hacer.

...
it("method::read must call N times for a server response of N news item to appendItem method", function(done) {

    //Creamos la respuesta del servidor
    var stubbedResponse = [
        {
            title: 'Docker Installation',
            link: 'http://docs.docker.com/installation/'
        },
        {
            title: 'SinonJS',
            link: 'http://sinonjs.org/'
        },
        {
            title: "Jasmine",
            link: 'http://jasmine.github.io/'
        }
    ];

    //Creamos una respuesta falsa de la siguiente llamada al método ajax de jQuery
    sinon.stub($, "ajax").yieldsTo("success", stubbedResponse);

    //Creamos un stub del metodo de añadir item (appendItem)
    var stubAppendItem = sinon.stub(newsReader, 'appendItem');

    //Llamamos al método read para que ejecute el código
    newsReader.read();

    //Esperamos que haya llamada tantas veces a appendItem como noticias hay en el array
    expect(stubAppendItem.calledThrice).toBe(true);

    //Comprobaciones de las llamadas
    var firstCall = stubAppendItem.firstCall;
    expect(firstCall.args[0].title).toBe("Docker Installation");
    expect(firstCall.args[0].link).toBe("http://docs.docker.com/installation/");

    var secondCall = stubAppendItem.secondCall;
    expect(secondCall.args[0].title).toBe("SinonJS");
    expect(secondCall.args[0].link).toBe("http://sinonjs.org/");

    var thirdCall = stubAppendItem.thirdCall;
    expect(thirdCall.args[0].title).toBe("Jasmine");
    expect(thirdCall.args[0].link).toBe("http://jasmine.github.io/");

    //Limpiar, restaurar los métodos originales
    newsReader.appendItem.restore();
    $.ajax.restore();

    done();
});
...

Los métodos dentro del test que nos permite hacer stub tanto del método ajax de jQuery como del appendItem de nuestra libreria són:

// En el momento de llamar al método $.ajax(...), Sinon ejecutará
// directamente el método 'success' del objeto argumento, enviado
// como primer argumento el objeto preparado stubbedResponse
sinon.stub($, "ajax").yieldsTo("success", stubbedResponse);
var stubAppendItem = sinon.stub(newsReader, 'appendItem');

Ahora tenemos las pruebas unitarias básicas para los métodos que hemos creado, podemos devolver respuestas con estructura incorrecta o errores de tipo 500 o 403, para ver como se comportan nuestros componentes de Javascript.

Leave a comment