Test end-to-end con NightwatchJS

1
3929

NightwatchJS nos provee de una forma sencilla de preparar nuestros test funcionales contra una interfaz web a través de Selenium o contra un webdriver concreto.

Índice de contenidos

1. Introducción

Enmarcado en el proceso de desarrollo mediante TDD, la parte backend suele estar cubierta mediante test unitarios y de integración. Y en la parte frontend, cada vez son más los proyectos que usan Jasmine, Karma o Mocha para cubrir su JavaScript con test. Pero a veces, nuestros proyectos adolecen de test funcionales que prueben una funcionalidad de principio a fin. NightwatchJS nos ayuda a solucionar ese problema.

En el ATDD incluso puede ser un punto de partida, pues en los diseños dirigidos por test de aceptación, precisamente hay que hacer un test que cubra el criterio de aceptación definido por el usuario, y lo normal es que el test implique una prueba de principio a fin.

[teaser img=»https://www.adictosaltrabajo.com/wp-content/uploads/2016/12/nightwatchJS.jpg»]
NightwatchJS nos permite hacer test end-to-end en los ciclos de integración continua y es un gran aliado en los procesos de diseño dirigido por test de aceptación (ATDD).
[/teaser]

Pero como me recuerda siempre un compañero, este tipo de test son los más frágiles, pues en cuanto cambia algo de todo el proceso, el test se rompe. Basta con que cambie el DOM de la página que queremos probar para que el test se rompa, aunque la funcionalidad se siga manteniendo. En este sentido, hay que saber lo que se tiene entre manos. En los equipos de desarrollo en los que está muy marcado el rol de quien desarrolla backend y quien desarrolla frontend, quizás la responsabilidad de mantener este tipo de test debería recaer sobre el equipo de front, pues un cambio en el DOM debería implicar una adaptación del test al cambio.

Pero primero veamos un ejemplo.

1. Primer test con NightwatchJS

Vamos a preparar un test que compruebe que cuando buscamos «autentia» en nuestros buscadores favoritos, aparece en la primera página y como primer resultado. Vamos a probarlo con Google y con DuckDuckGo.

module.exports = {

  'Busqueda en Google' : function (browser) {
    browser.url('https://www.google.es')   
      .waitForElementVisible('body', 2000)
      .setValue('#lst-ib', 'autentia')
      .click('input[type=submit]')
      .pause(2000)
      .assert.containsText('#rso > div', 'Autentia | Soporte a desarrollo informático')
      .end()
  },

  'Busqueda en DuckDuckGo' : function (browser) {
    browser.url('https://www.duckduckgo.com')   
      .waitForElementVisible('body', 2000)
      .setValue('#search_form_input_homepage', 'autentia')
      .click('input[type=submit]')
      .pause(2000)
      .assert.containsText('#r1-0 > div', 'Autentia | Soporte a desarrollo informático')
      .end()
  }
};

Y lanzamos el test…

[teaser img=»https://www.adictosaltrabajo.com/wp-content/uploads/2016/12/resultadosBuscadores.png»]
Efectivamente, en el test se comprueba que entre los resultados de búsqueda de ambos buscadores, aparece en primer lugar el título de la página web que estábamos buscando. Autentia aparece la primera en los dos.
[/teaser]

2. Instalación y configuración

NightwatchJS se instala mediante npm de una forma muy sencilla. En este caso, y como yo lo voy a usar desde distintos proyectos, prefiero hacerlo de forma global. De ahí el parámetro «-g»

npm install -g nightwatch

Luego hay que descargarse la version standalone del servidor de selenium. En el momento de escribir este artículo es la 3.0.1. Así que me creo un directorio llamado bin dentro de mi proyecto, donde guardaré el servidor de selenium y los webdrivers de los distintos navegadores. Me descargo también el webdriver de Chrome y de firefox (Gecko) para mi sistema operativo.

Para no hacer muy largo esta primera toma de contacto, voy a configurar sólo el servidor de Selenium y el WebDriver de Chrome. Creo el fichero nightwatch.json

{
  "src_folders" : ["tests"],
  "output_folder" : "reports",
  "custom_commands_path" : "",
  "custom_assertions_path" : "",
  "page_objects_path" : "",
  "globals_path" : "",

  "selenium" : {
    "start_process" : true,
    "start_session" : true,
    "server_path" : "./bin/selenium-server-standalone-3.0.1.jar",
    "log_path" : "",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "./bin/chromedriver"
    }
  },

  "test_settings" : {
    "default" : {
      "launch_url" : "",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : false,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "chrome",
        "javascriptEnabled": true,
        "acceptSslCerts": true,
        "cssSelectorsEnabled": true,
        "start-maximized": true,
        "chromeOptions": {
           "args": ["start-fullscreen" , "start-maximized"]
        }
      }
    }
  }
}

Con esto, indicamos cual es la carpeta donde van a estar escritos los test, donde se van a generar los informes, donde está el JAR de Selenium y el WebDriver de Chrome, y sus parámetros de configuración

[box style=»1″]

[icon icon=»exclamation»]NOTA:

En MacOS X me he topado con dos problemas. El primero es que el Terminal, hay que cambiar un check por defecto en «preferencias > avanzado» y desmarcar «Ajustar variables del entorno local al arrancar», y el segundo es por el ChromeDriver para MacOS X, que se arranca con una ventana con un ancho determinado, y puede que no sea válido para probar cuestiones relacionadas con responsive design. Para que arranque maximizado hay que añadir en chromeOptions la opción start-fullscreen y start-maximized, como aparece en el código anterior del nightwatch.json.

[/box]

3. Variables globales y entornos

Este tipo de pruebas se pueden querer enmarcar dentro de procesos de integración continua, por ejemplo con Jenkins, o simplemente queremos probar una batería de test contra distintos entornos. Lo primero de todo es definir dichos entornos en el nightwatch.json y cuales son sus URLs de acceso.

{
  [...],

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      [...]
    },
    "desarrollo" : {
      "launch_url" : "http://desarrollo.midominio.com"
    },
    "integracion" : {
      "launch_url" : "http://integracion.midominio.com"
    },
    "produccion" : {
      "launch_url" : "http://www.midominio.com"
    }

  }
}

De esta forma, nuestra batería de test se lanzarán contra uno u otro entorno usando el siguiente comando

nightwatch
nightwatch --env desarrollo
nightwatch --env integracion
nightwatch --env produccion

El problema es que nuestros test no están preparados para coger la URL propia del entorno. Además, los datos son independientes del entorno que se prueba. Imaginemos que lo que estamos probando es una operativa bancaria propia de una entidad financiera. El usuario, password y número de cuenta serán distintos dependiendo del entorno. Y esto deberíamos poderlo independizar de nuestros test. Para esto podemos definir variables globales por entorno.

{
  [...],

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      [...]
    },
    "desarrollo" : {
      "launch_url" : "http://desarrollo.midominio.com",
      "globals" : {
        "username" : "jjimenezt",
        "password" : "jjimenezt",
        "numeroCuenta" : "20380100220506078090"
      }
    },
    "integracion" : {
      "launch_url" : "http://integracion.midominio.com",
      "globals" : {
        "username" : "aramirez",
        "password" : "12345678",
        "numeroCuenta" : "20140100336566676094"
      }
    },
    "produccion" : {
      "launch_url" : "http://www.midominio.com",
      "globals" : {
        "username" : "azpilicueta",
        "password" : "_1zP3l3C52t1+",
        "numeroCuenta" : "15640100988392019283"
      }
    }

  }
}

Y nuestros test tendríamos que adaptarlos para que usen estas variables globales y así independizarlos del entorno en que se está probando

module.exports = {

  'Autenticacion en el Banco' : function (browser) {
    browser.url(browser.launchUrl)   
      .waitForElementVisible('body', 2000)
      .setValue('#username', browser.globals.username)
      .setValue('#password', browser.globals.password)
      .click('input[type=submit]')
  },

  'Selección de cuenta bancaria' : function (browser) {
    browser
      .waitForElementVisible('h1',2000)
      .assert.containsText('h1', 'Bienvenido')
      .setValue('#numeroCuenta', browser.globals.numeroCuenta)
      .click('#shop-send')
  },

  'Se comprueba que estamos en la cuenta seleccionada' : function(browser) {
    browser
      .pause(2000)
      .assert.containsText('.selectedAccount', browser.globals.numeroCuenta)
      .assert.containsText('#saldo', 'Su saldo es ')
      .end();
  }
};

4. Objetos de página

El tratamiento con el browser se hace mediante selectores CSS del DOM, y con él podemos realizar ciertas acciones (comandos) o afirmaciones (asserts). El caso, es que esos selectores, se pueden repetir mucho a lo largo de los test, y además son muy cercanos al DOM, mientras que probablemente los test de aceptación estén redactados en un lenguaje mucho más cercano al usuario o dueño del producto. Es por eso, que debemos emplear page objects. Martin Fowler explica muy bien porque es conveniente usar Page Objects

Los Page Objects se comportan como alias de selectores. En lugar de referirme a un elemento del DOM por su XPath o por selectores CSS, me refiero a él por un alias, de forma que

  • es más fácil de leer funcionalmente el test
  • si cambia el selector, sólo lo tenemos que cambiar en u sitio

Para usarlo sería suficiente indicar el directorio de los page objects en el nightwatch.json en el atributo page_objects_path

"page_objects_path" : "dirPageOptions"

En nuestro siguiente ejemplo, vamos a testear un cliente web de correo. Para ello vamos al directorio que hemos creado, en nuestro caso, dirPageOptions, y creamos el fichero correo.js

module.exports = {
  url: function() { 
    return this.api.launchUrl; 
  },
  elements: {
    username: { 
      selector: 'div.login > input[type=text]' 
    },
    password: {
      selector: 'div.login > input[type=password]'     
    },
    botonValidarUsuario: {
      selector: 'div.login > input[type=submit]'
    },
    tituloPagina: {
      selector: '.col-md-12 > h2.heading-page' 
    },
    botonRedactar: {
      selector: 'nav > .navbar-collapse > input[type=button]'
    },
    destinatario: {
      selector: '.mail > input[type=text].to'  
    },
    asunto: {
      selector: '.mail > input[type=text].subject'  
    },
    cuerpo: {
      selector: '.mail > textarea'  
    },
    botonEnviar: {
      selector: '.mail > input[type=submit]'
    },
    correosEnviados: {
      selector: 'nav > .navbar-collapse > div.sent'
    },
    listadoCorreosEnviados: {
      selector: '.col-md-12 > .content > ul'    
    }
  }
};

Luego en nuestro test vamos a probar que nos podemos autenticar bien, que podemos redactar un correo y enviarlo, y que éste aparece en la lista de correos enviados. El test quedaría como sigue:

module.exports = {
  before : function(browser) {
    ms = browser.globals.time;
  },

  'Login en el cliente de correo' : function (browser) {
    var correo = browser.page.correo();
    correo.navigate()
      .waitForElementVisible('body', ms)
      .setValue('@username', browser.globals.username)
      .setValue('@password', browser.globals.password)
      .click('@botonLogin')
      .waitForElementVisible('@tituloPagina',ms)
      .assert.containsText('@tituloPagina', 'Hola, Juan Antonio');
  },

  'Redactar un correo nuevo' : function (browser) {
    var correo = browser.page.correo();
    correo
      .click('@botonRedactar')
      .waitForElementVisible('@destinatario',ms)
      .waitForElementVisible('@asunto',ms)
      .waitForElementVisible('@cuerpo',ms)
      .setValue('@destinatario', browser.globals.correo.destinatario)
      .setValue('@asunto', 'Esto es un correo de prueba')
      .setValue('@cuerpo', 'Vamos a ver si esto llega')
      .click('@botonEnviar')
      .waitForElementVisible('body','Su correo ha sido enviado correctamente');
  },

  'Comprobar que el correo se ha enviado' : function(browser) {
    var correo = browser.page.correo();
    correo
      .click('@correosEnviados')
      .waitForElementVisible('@listadoCorreosEnviados',ms)
      .assert.containsText('@listadoCorreosEnviados','Esto es un correo de prueba');
  },  

  after : function(browser) {
    browser.end();
  }
};

No hay selectores y el test es mucho más legible. Con muy pocas líneas, y de forma clara, hemos probado como autenticarnos, enviar un correo y comprobar que se ha enviado.

NightwatchJS nos permite usar Chai para aumentar la verbosidad de las aserciones. Es algo así como hamcrest pero para JavaScript. De esta forma, nos acerca un poco más al estilo de aserciones propio del BDD.

[box style=»1″]

[icon icon=»smiley»]Probar aplicaciones web móviles con Appium y NightwatchJS

NightwatchJS está pensado para interactuar con navegadores. Y Appium para probar aplicaciones móviles, ya sean nativas, híbridas o web. Es posible configurar el nightwatch.json para que ambos programas colaboren, y poder probar nuestras aplicaciones web en móviles ya sean iOS o Android, e integrar las pruebas dentro del ciclo de integración continua. Este tema daría por sí sólo para otro artículo.

[/box]

Enlaces y referencias

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad