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

Explorando ArrayBuffer, DataView y matrices con tipo

Hasta hace relativamente poco en Javascript era complicado gestionar datos binarios. ArrayBuffer, DataView y las matrices con tipo (Typed Array) ponen a nuestra disposición un conjunto bastante completo de herramientas para manejar tipos binarios sin problemas. Vamos a ver cómo funcionan…

Objetos Map y Set

Los objetos Map y Set nos pueden ser de gran ayuda para gestionar conjuntos de datos, pudiendo simplificar nuestros programas en muchas circunstancias. Es interesante que sepamos cómo se utilizan y que pequeños secretos esconden. Vamos a revisarlos…

Referencia circular en objetos

Todos sabemos que los objetos pueden contener otros objetos, pero de lo que quizás no somos conscientes es que con mucha facilidad podemos crear una referencia circular, es decir, que si recorremos las propiedades del objeto y vamos profundizando, llegamos de nuevo al objeto inicial. Debemos tener en en cuenta esta circunstancia a la hora de realizar algunas operaciones o tendremos problema. Veamos cómo…

Características de las propiedades de los objetos

Existen varios tipos de propiedades que se comportan de forma diferente. Tenemos que tener en cuenta es la diferencia entre propiedades enumerables y no enumerables, propias y heredadas, de sólo lectura o no configurables, sin olvidar alguna que otra convención para definir propiedades como privadas. Veamos cómo trabajar con los distintos tipos de propiedades de un objeto.

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.