Seleccionar página

Uno de los contextos donde puede llegar a ser más útil el uso de co para gestionar la asincronía de Javascript es en los tests, ya que es habitual tener que realizar varios pasos uno detrás de otro e ir encadenando las acciones. En estos casos las llamadas con callbacks producen que la indentación del código vaya creciendo rápidamente haciendo nuestro código difícil de leer y comprender.

A continuación vamos ver cómo crear un conjunto de tests sobre un API REST fácilmente comprensibles con mocha como gestor de los test, chai como librería de comprobación, co para gestionar las llamadas sincrónicas y fetch para llamar al servidor por HTTP.

Introducción

Vamos a hacer una pequeña introducción sobre cada una de los elementos que vamos a utilizar. Si ya los conoces, puedes pasar directamente a la siguiente sección donde mostramos ejemplos concretos de su utilización.

Mocha

Es un popular gestor de tests en Javascript que funciona tanto en Node como en los navegadores. Su instalación es sencilla si disponemos por medio de NPM:

npm install mocha --save-dev

Para crear una test debemos encadenar llamadas a describe() para los grupos e incluir llamadas a it() para cada una de las pruebas. El callback de cada it() recibe una función done que ejecutaremos cuando hayamos realizado todas las comprobaciones.

Este es un ejemplo básico de Mocha:

describe('Array', function() {
    describe('#indexOf()', function() {
        it('Debe devolver -1 cuando el valor no está en la matriz', function(done) {
            console.assert(-1, [1,2,3].indexOf(4));
            done();
        });
    });
});

Para ejecutar el test debemos incluir este código en un fichero y utilizar la siguiente línea de comando (puede variar dependiendo de donde tengamos instalado Mocha, sobre todo si lo hemos instalado globalmente):

./node_modules/.bin/mocha test01.js

Mocha dispone de muchas más funcionalidades, pero con esta introducción es suficiente para avanzar. Si quieres saber más, te recomendamos que consultes: https://mochajs.org/.

Chai

Para hacer las comprobaciones en los tests podemos usar un sencillo console.assert(), como hemos utilizado en nuestro ejemplo anterior, también podemos usar el módulo assert que provee directamente Node, pero es bastante práctico utilizar una librería de comprobación avanzada como es Chai, que podemos instalar con:

npm install chai --save-dev

Chai dispone de varios interfaces con diferentes estilos de comprobación. Esto es muy práctico para utilizar el que más nos guste, pero suele confundir bastante a los que están empezado. Nosotros hemos elegido expect, no es ni mejor ni peor que las demás, simplemente es la que hemos utilizado.

Vamos a modificar nuestro ejemplo anterior para utilizar Chai:

const expect = require('chai').expect;

describe('Array', function() {
    describe('#indexOf()', function() {
        it('Debe devolver -1 cuando el valor no está en la matriz', function(done) {
            expect([1,2,3].indexOf(4)).to.be.a('Number');
            expect([1,2,3].indexOf(4)).to.be.equal(-1);
            done();
        });
    });
});

Hemos utilizado dos de los métodos que nos ofrece Chai para hacer las comprobaciones: to.be.a() o to.be.an() que permite comprobar tipos y to.be.equal() que permite comprobar valores. También es útil conocer to.be.deep.equal() para comprobar que dos objetos tienen los mismos valores en todas sus propiedades.

Chai ofrece muchas opciones y métodos para hacer comprobaciones de todo tipo. Si quieres conocerlas, consulta http://chaijs.com/.

Co

Para controlar la ejecución asíncrona en ES6 podemos utilizar co(), un interesante paquete que junto a las funciones generadoras nos ofrece una alternativa interesante al uso de callbacks. En futuras versiones de Javascript podremos utilizar aync/await. Si no conoces co() o aync/await, te recomendamos que leas el artículo Controlar la ejecucion asincrona.

Para instalar el paquete co debemos utilizar:

npm install co --save-dev

Hay que tener en cuenta que para que se comporte adecuadamente con Mocha siempre debemos incluir un catch() para la promesa que devuelve co() y en la función que pasemos a este catch() deberemos llamar a done() con el error que hemos recibido, de esta forma Mocha es informado de que se ha producido un error. Si nos olvidamos, no funcionará adecuadamente.

Vamos a ver un sencillo ejemplo:

const expect = require('chai').expect;
const co     = require('co');

describe('Promise', function () {
    describe('resolve()', function () {
        it('Debe devolver el valor pasado como parámetro', function (done) {
            co(function *() {
                var result = yield Promise.resolve('ok');
                expect(result).to.be.equal('ok');
                done();
            }).catch(function(err) {
                done(err);
            });
        });
    });
});

Si quieres conocer más te recomendamos que consultes https://github.com/tj/co.

fetch

Hay muchos paquetes para gestionar llamadas HTTP, pero la aparición de estándar fetch nos anima a utilizar esta solución, que funciona tanto en navegadores como en Node. Te recomendamos que leas nuestro artículo API fetch, el nuevo estándar que permite hacer llamadas HTTP. La gran ventaja, frente a otros paquetes, es que devuelve promesas y por lo tanto es muy adecuado para utilizarlo con junto con co().

Si lo vamos a utilizar desde Node tenemos que instalar el paquete node-fetch, que nos ofrece una implementación bastante completa del estándar:

npm install node-fetch --save-dev

A partir de aquí sólo tenemos que invocar a fetch() recordando que como primer parámetro recibe la URL a la que queremos acceder y como segundo parámetro un objeto con las opciones que se deben utilizar.

Aquí tenemos ver un ejemplo:

const expect = require('chai').expect;
const co     = require('co');
const fetch  = require('node-fetch');

describe('Fetch', function () {
    it('Debe devolver un objeto JSON', (done) => co(function *() {
        var response = yield fetch('http://jsonplaceholder.typicode.com/users/1');
        expect(response.status).to.be.equal(200);
        var user = yield response.json();
        expect(user).to.be.an('Object');
        done();
    }).catch(function (err) {
            done(err);
    }));
});

Nota: en este ejemplo hemos utilizado una arrow function o función flecha: (done) =>. Simplemente hace algo más compacto nuestro código, pero no tiene utilidad práctica.

Para conocer más sobre fetch() podemos consultar https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API.

Pruebas del API REST

Ya hemos introducido todos los elementos que vamos a utilizar y los hemos instalado en nuestro equipo, por lo que ya podemos centrarnos en desarrollar un conjunto de tests. Aquí sólo vamos a recoger algunos ejemplos básicos, pero como se podrá comprobar es sencillo ampliar estos tests con una mayor cobertura y complejidad.

Para los ejemplos vamos a utilizar los servicios de http://jsonplaceholder.typicode.com/ que nos ofrece un API REST de pruebas bastante sencilla. No tiene una utilidad real, no almacena los nuevos registros que añadimos o modificamos, pero permite construir ejemplos con rapidez.

Obtención de una matriz

Primer caso de prueba vamos a obtener todos los usuarios dados de alta. Es un caso muy habitual, obtener todos los recursos en una matriz y recorrer sus elementos comprobando que todos ellos son correctos.

const expect  = require('chai').expect;
const co      = require('co');
const fetch   = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
    it('GET /users debe devolver todos los usuarios', (done) => co(function *() {

        const response = yield fetch(SERVER + '/users');
        expect(response.status).to.be.equal(200);

        const users = yield response.json();
        expect(users).to.be.an('Array');
        for (let usr of users) {
            expect(usr).to.be.an('Object');
            expect(usr.id).to.be.a('Number');
            expect(usr.name).to.be.a('String');
            expect(usr.username).to.be.a('String');
            expect(usr.email).to.be.a('String');
        }

        done();
    }).catch(function (err) {
        done(err);
    }));
});

Encadenar varias llamadas al API

Ahora vamos a realizar varias llamadas una detrás de otra: primero consultaremos un usuario, obtendremos su id y consultaremos todos los posts de ese usuario. Para ello añadiremos este nueva llamada a it() dentro del describe() del ejemplo anterior, de esta forma vamos añadiendo casos de prueba a nuestro test.

it('GET /users/<id>/posts debe devolver todos los post del usuario', (done) => co(function *() {

    const responseUsers = yield fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
    expect(responseUsers.status).to.be.equal(200);

    const users = yield responseUsers.json();
    expect(users).to.be.an('Array');
    expect(users.length).to.be.equal(1);

    const responsePosts = yield fetch(SERVER + '/users/' + users[0].id + '/posts');
    expect(responsePosts.status).to.be.equal(200);

    const posts = yield responsePosts.json();
    expect(posts).to.be.an('Array');
    for (let p of posts) {
        expect(p).to.be.an('Object');
        expect(p.userId).to.be.equal(users[0].id);
        expect(p.id).to.be.a('Number');
        expect(p.title).to.be.a('String');
        expect(p.body).to.be.a('String');
    }

    done();
}).catch(function (err) {
    done(err);
}));

Método POST

No todo van a set métodos GET, vamos a ver un caso donde añadiremos un post con el método POST (parece un galimatías, pero el post se refiere a un artículo publicado por el usuario y el POST a un método HTTP; son la misma palabra, pero no tienen nada que ver). Para ello añadimos este it() a nuestro conjunto de tests:

it('POST /users/<id>/posts debe añadir un nuevo post al usuario', (done) => co(function *() {

    const responseUsers = yield fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
    expect(responseUsers.status).to.be.equal(200);

    const users = yield responseUsers.json();
    expect(users).to.be.an('Array');
    expect(users.length).to.be.equal(1);

    const responseNewPost = yield fetch(SERVER + '/users/' + users[0].id + '/posts', {
        method: 'POST',
        Headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            "title": "Lorem Ipsum",
            "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
                    "Etiam fringilla enim mi, a ornare augue sollicitudin id."
        })
    });
    expect(responseNewPost.status).to.be.equal(201);

    const post = yield responseNewPost.json();
    expect(post).to.be.an('Object');
    expect(post.userId).to.be.equal(users[0].id);
    expect(post.id).to.be.a('Number');

    done();
}).catch(function (err) {
    done(err);
}));

Diferencia de un test con y sin co()

Para mostrar la diferencia entre usar co() con fetch() y no usar co() y usar algún otro paquete basado en callbacks como el popular request, vamos a actualizar un post del usuario. Como en los ejemplos anteriores tendremos que llamar varias veces al servidor de forma consecutiva.

En primer lugar vamos a ver el test con request():

const expect  = require('chai').expect;
const request = require('request');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
    it('PUT /posts/<id> - debe modificar un post (con request)', function (done) {

        request(SERVER + '/users?email=Rey.Padberg@karina.biz', {
                method: 'GET',
                json: true
            },
            function (err, responseUsers) {
                expect(err).to.be.equal(null);
                expect(responseUsers.statusCode).to.be.equal(200);
                expect(responseUsers.body).to.be.an('Array');
                expect(responseUsers.body.length).to.be.equal(1);

                request(SERVER + '/users/' + responseUsers.body[0].id + '/posts', {
                        method: 'GET',
                        json: true
                    },
                    function (err, responsePosts) {
                        expect(err).to.be.equal(null);
                        expect(responsePosts.statusCode).to.be.equal(200);
                        expect(responsePosts.body).to.be.an('Array');

                        request(SERVER + '/posts/' + responsePosts.body[0].id, {
                                method: 'PUT',
                                body: {
                                    "title": "Lorem Ipsum",
                                    "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
                                            "Etiam fringilla enim mi, a ornare augue sollicitudin id."
                                },
                                json: true
                            },
                            function (err, responseEditPost) {
                                expect(err).to.be.equal(null);
                                expect(responseEditPost.statusCode).to.be.equal(200);
                                expect(responseEditPost.body).to.be.an('Object');
                                expect(responseEditPost.body.id).to.be.equal(responsePosts.body[0].id);
                                done();
                            }
                        );
                    }
                );
            }
        );
    });
});

Ahora vamos a ver el mismo test con co() y fetch():

const expect = require('chai').expect;
const co     = require('co');
const fetch  = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
    it('PUT /posts/<id> debe modificar un post', (done) => co(function *() {

        const responseUsers = yield fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
        expect(responseUsers.status).to.be.equal(200);

        const users = yield responseUsers.json();
        expect(users).to.be.an('Array');
        expect(users.length).to.be.equal(1);

        const responsePosts = yield fetch(SERVER + '/users/' + users[0].id + '/posts');
        expect(responsePosts.status).to.be.equal(200);

        const posts = yield responsePosts.json();
        expect(posts).to.be.an('Array');

        const responseEditPost = yield fetch(SERVER + '/posts/' + posts[0].id, {
            method: 'PUT',
            Headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                "title": "Lorem Ipsum",
                "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
                        "Etiam fringilla enim mi, a ornare augue sollicitudin id."
            })
        });
        expect(responseEditPost.status).to.be.equal(200);

        const post = yield responseEditPost.json();
        expect(post).to.be.an('Object');
        expect(post.id).to.be.equal(posts[0].id);

        done();
    }).catch(function (err) {
        done(err);
    }));
});

Como se puede comprobar la versión que utiliza co() es bastante más sencilla de leer que el modelo basado en callbacks. Este es realmente un ejemplo sencillo, ya que en ocasiones tendremos que realizar pruebas mucho más complejas, realizando varias llamadas y finalmente borrando todos los elementos que hemos creado a fin de mantener las pruebas aisladas unas de otras. En esos casos más complejos la utilización de co() se vuelve aun más recomendable.

Conclusiones

En primer lugar debemos volver a insistir en las ventajas que nos ofrece fetch() al estar implementado de forma nativa en muchos navegadores y disponer de un paquete que nos permite utilizar este API en Node. De esta forma podemos hacer pruebas isomórficas que se ejecuten igual en el servidor y en el cliente. Además, el hecho de que devuelve promesas nos ayuda en la gestión de la asincronía de las llamadas al servidor.

Para el control de la ejecución asíncrona seguramente la mejor solución será utilizar el próximo async / await, pero mientras llega, el uso de co() con las promesas de fetch() son una buena solución. Nuestro código es más sencillo de leer y de mantener que con callback anidados.

Aquí sólo hemos mostrado unos ejemplos sencillos, pero creemos que se puede ver con claridad cómo es el mecanismo a utilizar para desarrollar este tipo de pruebas. Esperamos que sean de utilidad y sirvan de base para elaborar pruebas completas de las API REST.

Novedades

JSDayES en directo

Sigue en directo las charlas y talleres del impresionante JSDayES 2017. Ponentes de primer orden, tanto nacionales como internacionales, nos mostrarán todos los aspectos de Javascript. El viernes 12 de mayo de 16:00 a 21:00 y el sábado 13 de mayo de 9:30 a 21:00 (hora de Madrid).

The CITGM Diaries by Myles Borins [video]

Myles Borins de Google, miembro del CTC de Node.js (Node.js Core Technical Committee), nos cuenta (en inglés, por supuesto) como funciona CITGM (Canary In The Gold Mine), una herramienta que permite obtener un módulo de NPM y probarlo usando una versión específica de NodeJS.

Debate: Tecnologías de Front Web [vídeo]

Desde las principales comunidades de desarrollo de tecnologías de front (Madrid JS, Polymer Madrid, Angular Madrid y VueJS Madrid) se ha organizado este debate que pretende ser un ejercicio de sentido común en relación a las tecnologías de front actuales centradas en componentes.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Algunos operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.

Cómo diferenciar arrow function de function

En un reciente artículo Javier Vélez Reyes hace patente las principales diferencias entre las funciones tradicionales y las funciones flecha, ya que ambos modelos no son equivalentes e intercambiables. Veamos cómo es posible saber si una función ha sido construida por medio de la instrucción function o como una arrow function.