Test end-to-end con NightwatchJS

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://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://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

Comentarios

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

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

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

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Juan Antonio tiene más de 20 años de experiencia en el desarrollo del software. Curioso, y con una predisposición a la abstracción matemática, no deja de aprender. Consolida su aprendizaje en forma de tutoriales en este portal. Que además son fruto de su afán por devolver a la comunidad parte del conocimiento adquirido. Trabaja como arquitecto de software en Autentia. Y acaba de publicar su primera novela: Mutagénesis Convergente

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

02/03/2026

José Antonio Sánchez Segovia

Zephyr es un RTOS open source respaldado por la Linux Foundation que permite desarrollar dispositivos embebidos conectados, eficientes y escalables, facilitando el paso de prototipo a producto final con una arquitectura mantenible.

23/02/2026

Enrique Casado Díez

LoRa y LoRaWAN son tecnologías clave en el ecosistema IoT cuando se requiere largo alcance y bajo consumo energético. En este artículo analizamos su funcionamiento, Spreading Factor, link budget, arquitectura de red, frecuencias y clases de dispositivos, con un caso práctico real.

19/02/2026

Juan José Díaz Antuña

Copilot Chat es la forma más sencilla y segura de empezar a usar IA en Microsoft 365. En este artículo vemos cómo funciona, cómo activarlo y en qué se diferencia de Microsoft 365 Copilot, Copilot Studio y los Agentes Inteligentes, con ejemplos prácticos y una comparativa clara.