viernes, 7 de abril de 2017

Behavior-Driven server testing


Imagina que debes mantener una configuración compleja en un servidor, por ejemplo, web. Logras configurar unas reglas para una ruta y cuando vas a afrontar las siguientes reglas quieres estar seguro que no estás alterando el comportamiento de lo que ya has hecho. ¿Vuelves a probar a mano todos los casos con cada cambio?


Propongo abordar la situación desde una perspectiva BDD o Desarrollo guiado por comportamiento que puede servir para cualquier servidor (Apache, nginx, ...). Me centraré en los test y lo justo para ejecutarlos, sin llegar a profundizar en conceptos teóricos ni cómo instalar las herramientas.

Descripción

Supongamos una configuración sencilla:

- Al acceder a http://dominio.com/ desde un smartphone debo ver la versión móvil ubicada en http://dominio.com/m
Cuando accedo a http://dominio.com/ desde un smartphone
Entonces me redirije a http://dominio.com/m
- Y a la inversa, al acceder a http://dominio.com/m desde un navegador de escritorio debo ver la versión de escritorio ubicada en http://dominio.com/
Cuando accedo a http://dominio.com/m desde el pc
Entonces me redirije a http://dominio.com/
En realidar, hay tantos test como configuraciones puedas hacer, otros ejemplos sencillos pueden ser:
Cuando cargo un archivo .svg
Entonces debe verse correctamente
Cuando accedo a una dirección que no existe
Entonces me indica el código de estado correspondiente

Herramientas

Como no encontré nada que me convenciera mucho, usé lo que tenía a mano y me resultaba familiar. Ésto es sólo una sugerencia de cómo podrías implemetarlo.

- Servidor nodejs local
- Jasmine

En el package.json además de jasmine será necesaria la dependencia request.

Definición test

Los test se crean dentro de la carpeta spec, en un archivo o varios. Éstos podrían ser los correspondientes a los casos definidos anteriormente.

spec/redireccionesSpec.js
'use strict';
var request = require('request');

describe('access to http://dominio.com/', function() {
  describe('with smartphone', function() {
    it('should redirect to http://dominio.com/m', function(done) {
      // 'done' es necesario porque se realizarán llamadas asíncronas al servidor y necesitaremos indicar a Jasmine cuándo ha terminado la llamada.

      // Se configuran las opciones para ésta petición al servidor. En éste caso, acceder mediante un iPhone a http://dominio.com
      var options = {
        url: 'http://dominio.com',
        followredirect: false,
        headers: {
          'User-Agent': 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3'
        }
      }

      // Se realiza la petición al servidor
      request.get(options, function(error, response, body) {
        /* Si la conexión con el servidor fallara, un timeout, por ejemplo, Jasmine se detendría completamente,
           por lo tanto, si la conexión es correcta se define el test esperado, en caso contrario,
           se indica que se esperaba una conexión correcta. */
        if (error === null) {
          // Se define el comportamiento esperado, en éste caso, una redirección a http://dominio.com/m
          expect(response.statusCode).toBe(302);
          expect(response.headers.location).toBe('http://dominio.com/m');
        } else {
          expect(error).toBe(null);
        }
        // Y finalmente, se indica a Jasmine que se ha terminado la petición al servidor.
        done();
      });
    });
  });
  
  // Acceso a http://dominio.com/ usando un navegador de escritorio
  describe('with desktop browser', function() {
    it('should show web on http://dominio.com/', function(done) {
      var options = {
        url: 'http://dominio.com',
      }

      request.get(options, function(error, response, body) {
        if (error === null) {
          // Página encontrada y, por ejemplo, se comprueba alguna parte del contenido
          expect(response.statusCode).toBe(200);
          expect(body).toMatch(/Página versión escritorio/g);
        } else {
          expect(error).toBe(null);
        }

        done();
      });
    });
  });
});

describe('access to http://dominio.com/m', function() {
  describe('with smartphone', function() {
    it('should redirect to http://dominio.com/m', function(done) {
      var options = {
        url: 'http://dominio.com/m',
        headers: {
          'User-Agent': 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3'
        }
      }

      request.get(options, function(error, response, body) {
        if (error === null) {
          // Página encontrada y, por ejemplo, se comprueba alguna parte del contenido
          expect(response.statusCode).toBe(200);
          expect(body).toMatch(/Página versión smartphone/g);
        } else {
          expect(error).toBe(null);
        }

        done();
      });
    });
  });
  
  // Acceso a http://dominio.com/ usando un navegador de escritorio
  describe('with desktop browser', function() {
    it('should show web on http://dominio.com/', function(done) {
      var options = {
        url: 'http://dominio.com/m',
        followredirect: false,
      }

      request.get(options, function(error, response, body) {
        if (error === null) {
          // Redirección a la versión de escritorio
          expect(response.statusCode).toBe(302);
          expect(response.headers.location).toBe('http://dominio.com/');
        } else {
          expect(error).toBe(null);
        }
        done();
      });
    });
  });
});
spec/mimetypesSpec.js
'use strict';
var request = require('request');

describe('load .svg image', function() {
  it('should return image/xml+svg content-type', function(done) {
    var options = {
      url: 'http://dominio.com/images/logo.svg',
    }

    request.get(options, function(error, response, body) {
      if (error === null) {
        expect(response.headers['content-type']).toBe('image/svg+xml');
      } else {
        expect(error).toBe(null);
      }

      done();
    });
  });
});
spec/statusCodesSpec.js
'use strict';
var request = require('request');

describe('access non-existent url', function() {
  it('should return 404 status code', function(done) {
    var options = {
      url: 'http://dominio.com/noexiste/noexiste.html',
    }

    request.get(options, function(error, response, body) {
      if (error === null) {
        expect(response.statusCode).toBe(404);
      } else {
        expect(error).toBe(null);
      }

      done();
    });
  });
});

Aunque aquí he primado la legibilidad de cada caso, al final no dejan de ser unas cuantas líneas escritas con JS, por lo que se pueden usar variables y métodos para no tener tanto código duplicado.

Ejecución

En la carpeta del proyecto podrás ejecutar (linux/unix)
$ ./node_modules/.bin/jasmine
En el output de Jasmine se ve cuáles han fallado y se pueden ejecutar tantas veces como quieras. Además, si necesitas modificar la configuración de tu servidor, podrás definir nuevos test y probar que el resto sigue funcionando correctamente.

No hay comentarios:

Publicar un comentario