testing

Testing end to end (e2e) con WebdriverJS + NodeJS + Mocha + should.js

El testing en el desarrollo de software es una de las corrientes que más éxito está teniendo en los últimos tiempos. Las nuevas metodologías de desarrollo basado en test (TDD) o en comportamiento (BDD) permite crear aplicaciones más robustas y centradas en los requisitos más que en el contenido propio del código.

Dentro del testing se podría hacer una clasificación simple entre tres tipologías de test:

  • El testing unitario o de componente, se ocupa de probar las partes individuales en las que se puede dividir un código: si una clase o métodos funcionan correctamente o una parte determinada del programa. Ayuda a desarrollar partes concretas del código pero que no tienen que ver de forma directa con lo que el usuario espera del producto final. Por ejemplo podemos hacer un test para comprobar que un generador de números aleatorios funciona correctamente, pero solamente se comprueba una parte del sistema de seguridad que lo utiliza y que es lo que finalmente le interesa al usuario.
  • El testing de integración, para comprobar que los componentes reaccionan de forma adecuada a la hora de unirse. Es decir, controlan las interfaces entre los componentes o subsistemas.
  • El testing de fin a fin (end to end o e2e) es el que se ocupa de probar la aplicación final “de principio a fin”. Conceptualmente tiene mucho más que ver con lo que percibe el usuario del producto, que es al fin y al cabo la misión de nuestros programas: satisfacer al usuario y sus requisitos. Aspectos como el funcionamiento, el rendimiento o en consumo de recursos pueden ser comprobados con este tipo de test. En una página Web es especialmente relevante ya que debe responder de forma adecuada a todos los eventos de navegación que se producen en ella.

Una cosa que no debemos olvidar es que el testing solamente prueba que un software falla bajo determinadas condiciones, pero no indica que es totalmente fiable. Que un software funcione un número consistente de veces bajo unas condiciones no quiere decir que exista una posibilidad, aunque remota, de que falle bajo esas mismas condiciones. Se trata de que el número de fallos sea el menor posible, pero que si no falla, puede que lo haga en el futuro, sobre todo porque quizá no hayamos conseguido las condiciones de test adecuadas sobre las que se produce el fallo.

Dentro de los test “end to end”  (e2e),  están los test de comportamiento, que se encargan de comprobar que el programa (o aplicación Web en nuestro caso) funciona de forma correcta en cuanto al comportamiento: hacen pruebas como si se tratara de un usuario real. Es evidente que los test de comportamiento son muy adecuados para las aplicaciones Web actuales en las que la interacción juega un papel muy importante.

Un ejemplo de test de comportamiento podría ser el siguiente:

  • Accede a la página http://www.rusizate.com
  • Comprueba que el título de la página contiene la cadena de texto “Rusizate” [test1]
  • Localiza el botón de menú que da acceso a los contenidos para turistas y que tiene la clase “boton-189”.
  • Pulsa sobre el botón seleccionado
  • Comprueba que el título de la página que aparece contiene el texto “turistas” [test2]
  • etcétera…

La descripción del test debería ser lo más exhaustiva posible. Cuantos más aspectos probemos, más seguros estaremos de que la aplicación cumple con los requisitos que se han establecido, y también podremos dirigir nuestros desarrollos de un modo más adecuado a las necesidades del usuario final.

Para hacer los test end to end de nuestras aplicaciones web basadas en comportamiento se utilizará Selenium – Webdriver, que prové de una interfaz para poder manejar los navegadores más populares. Cada navegador dispone de un “driver” para que puedan ser manipulados por código como si de un usuario final se tratase. De este modo Internet Explorer, Chrome, Firefox, Safari u Opera disponen de drivers para que puedan ser utilizados por Webdriver.

Existen diferentes lenguajes para usar Webdriver: Java, Python o Javascript. Bajo mi punto de vista lo mejor es utilizar un lenguaje interpretado que pueda ser editado fácilmente. Los lenguajes compilados requieren el paso intermedio de compilación a ejecutable (o bytecode en Java), lo que ralentiza excesivamente la creación y depuración de los test. Los test no suelen ser programas complejos, sino que son una enumeración de los puntos que se deben cumplir, por lo que no es necesario contar con la potencia y complejidad de lenguajes compilados.

Un lenguaje intepretado que se puede utilizar con Webdriver es Javascript, que gracias a NodeJS, basado en el motor V8 de Google, está ganando enteros dentro del mundo del desarrollo. Gracias a NodeJS es posible ejecutar Javascript en la parte del cliente con un buen rendimiento, así como aprovecharse de su sistema de librerías NPM (Node Package Manager), que facilita la instalación y utilización de nuevas características que extienden la potencia de este entorno.

Volviendo a Selenium-Webdriver, existe un paquete descargable a través de NPM que instala todo lo que necesitamos de selenium-webdriver. Además deberemos descargar el driver de Chrome, llamado Chromedriver.exe, que será la interfaz a partir de la cual se manejará Google Chrome para realizar nuestras pruebas e2e. También vamos a utilizar Mocha, que es un framework de testing en Javascript, que facilitará la realización de nuestras pruebas, a través de funciones y comprobaciones que lleva incorporadas. Para la comprobación de los asserts de los test se empleará should.js, también disponible a través del sistema NPM.

Preparando el entorno

Prerequisitos:

  • Tener instalada una versión reciente de nodejs
  • Acceso a una shell.

Creando el directorio e instalando librerías:


mkdir testing
cd testing

npm install -g mocha
npm install selenium-webdriver
npm install should

Instalando el driver para Google Chrome

1. Acceder a http://chromedriver.storage.googleapis.com/index.html y descargar la última versión correspondiente al sistema operativo.

2. Hacer que el chromedriver.exe esté en el path del sistema: en windows hay que ir a panel de control->sistema->variables de entorno

3. Reiniciar la shell para que admita el nuevo cambio.

Creando el fichero testRus.js que contiene el test

Descargar el fichero de GitHub clonando el respositorio :

git clone https://github.com/4lberto/test_e2e.git

O simplemente copiar y pegar el código que pongo más abajo.

Ejecución del test

mocha testRus.js

Si todo ha ido bien y la página que se va a testear no ha cambiado, aparecerá una ventana de Google Chrome en nuestro escritorio y se realizarán las comprobaciones, como si un vídeo de screencast estuviéramos viendo. Debería pasar los 3 test de los que se compone el código de testRus.js. Debería ser algo así:

 

 

Comentarios sobre el código y el funcionamiento de WebdriverJS

Promises

Una característica de webdriveJS que tiene Javascript y que no tienen otro tipo de lenguajes para manejar Webdriver, como por ejemplo con Java, es el manejo de la naturaleza asíncrona de un navegador Web y de Javascript. Cuando cargamos una página y hacemos click en un botón, puede suceder que pase un tiempo hasta que la consecuencia de la acción se ha producido o Webdriver ha sido capaz de resolver el resultado de la instrucción. Es decir, si pedimos el contenido HTML de un elemento que hemos localizado a través de su clase, puede que este contenido HTML no esté disponible al instante. Si este contenido es utilizado en la siguiente instrucción puede que no pueda usarse todavía y la aplicación falle.

Para tratar esta asincronía, los creadores de WebdriverJS han recurrido a uná técnica llamada “promises”. Básicamente consiste en que cuando se solicita una información que puede que no esté disponible al instante, se devueelve de forma inmediata un objeto “promise” de esa información, es decir, un objeto que representa el objeto prometido, pero que no tiene por qué tener esta información. Este objeto será completado con la información correspondiente en cuanto sea posible, y será en ese momento cuando lance una notificación a sus observadores. ¿Quién son sus observadores? Pues son dos funciones: una que se lanza si todo ha ido correcto (no ha habido problemas a la hora de cargar la información); y otra función que se lanzará su ha habido algún error o problema.

En código se ve mucho mejor el funcionamiento del objeto promise:

var prom = driver.findElement(webdriver.By.className('miClase')).getInnerHTML();
prom.then(function(){//función correcta}, function(){//función error} );

Como se puede ver, en la variable “prom” queda alojado el objeto de tipo promise que debería tener alojado el contenido HTML del tag que tiene como clase “miClase”. Este objeto, en la instrucción siguiente, se le añaden dos observadores, que son dos funciones anónimas. Cuando el objeto “prom” disponga de la información real y no haya habido problemas, hará una llamada a la primera función anónima. Es obvio que es el método “then” el que nos ayuda a establecer ambas funciones observadoras que serán notificadas de la carga correcta o incorrecta de la información.

Por ejemplo, si queremos mostrar por pantalla el título de una página que se acaba de cargar podemos hacer:

var titulo = driver.getTitle();
titulo.then(function(titulo)
{
    console.log("título de la página:" + titulo);
}, function(){
    console.log("error");
});

Nuevamente, la variable titulo actúa como promise, pero su valor cuando pasa por esa instrucción no tiene por qué ser conocido. Sin embargo en la siguiente instrucción, con la propiedad “then” del promise, se registran dos funciones que serán llamadas cuando el objeto promise reciba el valor adecuado.

Por tanto, los promises son una forma de tratar con la asincronía existente en Javascript de forma elegante y nos permitirá amoldarnos a los eventos de la página.

Función wait()

Otro método que tiene Webdriver para tratar con los problemas de sincronía es el método driver.wait(función, tiempo_time_out). Este método espera hasta el tiempo_time_out para continuar con la ejecución, o hasta que se cumpla una condición (sea verdadera).

Se entenderá mejor con un ejemplo: si tenemos un botón que hace una llamada AJAX para mostrar una información que se recibe desde el servidor, lo que incluye cierto retardo en responder (pongamos 1 segundo), y muestra el resultado en el contenido de una capa, no nos servirá de mucho el sistema de promises, puesto que el contenido de la capa puede estar perfectamente definido (y tratado) aún cuando aún no ha llegado la respuesta desde el servidor. Es decir, puedo tener una capa vacía en la que se carga el resultado de la llamada. Con el sistema de promises puedo recuperar que el contenido efectivamente es vacío pero no podía esperar a recibir el contenido AJAX.

Utilizando la función wait podemos hacer un código parecido al siguiente:

driver.wait(function(){
    return driver.findElement(webdriver.By.id('resultado')).getInnerHTML().then(function(html)
    {
        return html.length>0;
    }
}, 3000).then(
    function()
    {
        //test del contenido...
    },
    function()
    {
        //trata el timeout...
    }
);

Esta función lo que haces es entrar en un bucle de comprobación constante que ejecuta la función de clausura que está en su interior y que comprueba que hay algo dentro del contenido HTML (con length>0) de un elemento cuyo id es “resultado”. Esto es, comprueba constantemente a ver el resultado de esa comprobación. Mientras sea false sigue haciendo comprobación hasta que encuentre un true o por el contrario pasen los 3000 milisegundos:

  • Si ha encontrado una salida positiva a su función de comprobación, ejecutará la primera de las funciones.
  • Si ha agotado el tiempo de time-out, ejecuta la segunda función, que es al función de error y debería tratar el problema.

Una vez han sido establecidos los antecendentes, vamos a pasar a ver el código del test sobre la página rusizate.com. Se trata de un test sencillo y nada exhausitivo, pero que debería darnos una idea sobre cómo se hacen este tipo de test. Los pasos son los siguientes:

  1. Acceso a la página y que se pulse el botón de aceptar las condiciones del uso de cookies, comprobando que una vez pulsado la capa de aviso que aparece, ha desaparecido.
  2. Salta a la página “Para gente curiosa”, comprobando que el título de la página que sucede a este salto es el adecuado.
  3. Utiliza el buscador, introduciendo el término “hermitage” y comprobando que el número de resultados es mayor 0.

Como he mencionado antes, el framework que se va a utilizar para hacer el testing es Mocha y las comprobaciones se van a hacer utilizando should.js. Todas estas tecnologías emplearán el driver para Google Chrome de Webdriver y se ejecutarán en nodejs.

El código es el siguiente:


var assert = require('assert'),
should = require('should'),
fs = require('fs');

var webdriver = require('../node_modules/selenium-webdriver/'),
test = require('../node_modules/selenium-webdriver/testing');
test.describe('Rusizate', function() { // Descripción del test
var driver;

//Antes de la ejecución de la prueba
test.before(function() {
    driver = new webdriver.Builder().
    withCapabilities(webdriver.Capabilities.chrome()).
    build();
});

//Test 1: accede a la página y acepta las condiciones legales
test.it('Acceso y aceptación de condiciones', function() {
    driver.get('http://www.rusizate.com');
    driver.findElement(webdriver.By.id('ca_banner')).then(function(div_banner)
    {
        div_banner.findElement(webdriver.By.className('accept')).click();
        driver.wait(function()
        {
            return div_banner.getCssValue("display").then(function(valor){
            return valor.indexOf("none")>-1;
        });
     }, 3000).then(function()
     {
        div_banner.getCssValue("display").then(function(valor){
        valor.should.equal("none");
     });
    });
    });
   });

//Test 2: Salta a la página "Para la gente curiosa"
test.it('Salta a Gente Curiosa', function() {
    driver.findElement(webdriver.By.className('item-117')).click();
    driver.wait(function() {
        return driver.getTitle().then(function(title) {
            return title.indexOf("curiosa")>-1;
        });
    }, 3000).then(function()
    {
        driver.getTitle().then(function(titulo)
        {
            titulo.should.include('curiosa');
        });
    });
 });

//Test 3: Utiliza el buscador y comprueba que devuelve 2 resultados
test.it('Buscador - buscar la palabra Hermitage', function() {
    driver.findElement(webdriver.By.id('mod-search-searchword')).sendKeys('hermitage');
    driver.findElement(webdriver.By.className('form-inline')).submit();

    driver.wait(function() {
        return driver.getTitle().then(function(title) {
        return title.indexOf("Buscar")>-1;
    });
 }, 10000).then(function()
 {
     driver.findElements(webdriver.By.tagName('dt')).then(function(resultados)
     {
         resultados.length.should.be.above(0); //Encuentra más de 1 resultado.
     });
 });
 });

//Cierra la instancia al finalizar la prueba
test.after(function() {
    driver.quit();
 });
});

Como puntos a destacar:

  • Carga de los módulos necesarios de webdriver, que no están instalados de forma global sino en un directorio por debajo del que estamos trabajando.
  • La descripción del test y sus fragmentos que se producen antes del test (test.before…) y después del test (test.afeter…) que permiten inveocar al driver y cerrarlo respectivamente.
  • Cada una de las pruebas que se llevan a cabo a través de test.it(descripción, funcion).

Entrando en detalle sobre el primer test, por que el resto cson cosas estándar, tenemos el acceso a la página web y la búsqueda de la capa en la que se muestra el aviso de conformidad con las cookies. Una vez localizado el elemento utilizando el ID, hay que localizar el botón que está dentro. Como se ha comentado antes, es información que puede estar disponible de forma asíncrona, asi que hay que usar los promises y su propiedad “then”.

Una vez localizado el botón se hace click sobre el elemento. Como la acción que esto desencadena sobre la interfaz consiste en que la capa que muestra el mensaje desaparece poco a poco, no se puede comprobar el efecto de manera inmediata.

Para esperar el tiempo necesario se emplea el método “wait”, comprobando constantemente durante 3 segundos a ver si ha desaparecido o no la capa (con display none). La comprobación posterior es redundante y consiste en cuando todo ha ido bien, es decir, que ha desaparecido la capa antes de los 3 segundos del time out, realiza una acción. En este caso la acción consiste en volver a comprobar el display:none. Esto en un escenario real podíamos decir que está repetido, pero se ha dejado por motivos didácticos.


//Test 1: accede a la página y acepta las condiciones legales
test.it('Acceso y aceptación de condiciones', function() {
    driver.get('http://www.rusizate.com');
    driver.findElement(webdriver.By.id('ca_banner')).then(function(div_banner)
    {
        div_banner.findElement(webdriver.By.className('accept')).click();
        driver.wait(function()
        {
            return div_banner.getCssValue("-display").then(function(valor){
                return valor.indexOf("none")>-1;
            });
         }
         , 3000).then(function()
         {
             div_banner.getCssValue("display").then(function(valor){
                 valor.should.equal("none");
          });

      });
  });
});

En cuanto al segundo paso no puede ser más sencillo: se localiza en la página el botón que da acceso a la página para la gente curiosa (localizado con un class unívoco), y hacemos click.

Del mismo modo que antes, se tiene que esperar un tiempo hasta que se produzca el evento de carga, que bien puede ser que espere cargando la página hasta que aparezca la palabra “curiosa”. Como en el caso anterior, la comprobación del test vuelve a ser la misma que la condición de salida.

 //Test 2: Salta a la página "Para la gente curiosa"
test.it('Salta a Gente Curiosa', function() {
    driver.findElement(webdriver.By.className('item-117')).click();
    driver.wait(function() {
        return driver.getTitle().then(function(title) {
            return title.indexOf("curiosa")>-1;
        });
     }, 3000).then(function()
     {
         driver.getTitle().then(function(titulo)
         {
             titulo.should.include('curiosa');
         });
     });
});

Finalmente vamos a ver cómo se realiza una búsqueda. Es muy sencillo. Simplemente se localiza el campo de texto donde se introducen los valores y se invoca el método “sendKeys” que imita la introducción por teclado de una cadena de texto. A continuación se localiza el formulario y se invoca el método “submit”.

De nuevo queda esperar a la carga de resultados con la comprobación del título de la página durante 10 segundos. Una vez tenemos la página de resultados se buscan los elementos de resultado (esta vez en plural) a través del nombre del tag “dt” (previamente sabemos que estos tag sólo se usan por los resultados). Usando should.js hacemos la comprobación de que hay más de un resultado.


//Test 3: Utiliza el buscador y comprueba que devuelve 2 resultados
test.it('Buscador - buscar la palabra Hermitage', function() {
    driver.findElement(webdriver.By.id('mod-search-searchword')).sendKeys('hermitage');
    driver.findElement(webdriver.By.className('form-inline')).submit();

    driver.wait(function() {
        return driver.getTitle().then(function(title) {
            return title.indexOf("Buscar")>-1;
        });
    }, 10000).then(function()
    {
        driver.findElements(webdriver.By.tagName('dt')).then(function(resultados)
        {
            resultados.length.should.be.above(0); //Encuentra más de 1 resultado.
        });
    });
});

Para ejecutar el test, como se ha dicho antes,  simplemente hay que ejecutar en la línea de comandos “mocha testRus.js” y deberían pasar los 3 test indicados. Si en alguno de ellos no fuera positivo el resultado, devolverá un aviso indicando qué test ha fallado y por qué razón gracias a Should.js y Mocha.

Finalmente se cierra la instancia del driver de Chrome para Webdriver.

Espero que este post os anime a mejorar la calidad de vuestras aplicaciones (y páginas) Web mediante la utilización de sistemas de testing unitarios (quizá empleando Karma) y testing finales como es el caso de Webdriver.

Anuncios