Nightwatch-Cucumber para pruebas de aceptación, ¿te atreves?

Cualquiera de nosotros, de los que nos dedicamos de un modo u otro al mundo de las tecnologías de la información, sabemos que en cualquier desarrollo de software mínimamente serio hay una etapa de pruebas de aceptación.

Las pruebas de aceptación de usuario (UAT) tienen como objetivo asegurar que el software se comporta en base a unos requerimientos escritos previamente los cuales han sido definidos con el fin de cubrir las necesidades del cliente.

Como podéis imaginar, es una etapa sumamente importante puesto que es uno de los grandes indicadores que tenemos para saber si estamos haciendo las cosas como es debido.

No pretendo hablaros de cómo definir correctamente esos requerimientos, eso da para otro post (y de los largos), sino que el objetivo es daros a conocer un modo de implementar esas pruebas con uno de los framework que llevo utilizando desde hace tiempo. Estoy hablando de Nightwatch-Cucumber.

Antes de que entremos a analizar sus características, os adelanto que no se trata de un único framework, sino que en realidad hace uso de… ¡otros dos! El primero de ellos, y que vamos a explicar, es NightwatchJS.

NightwatchJS es un framework para implementar pruebas automáticas de aplicaciones y sitios web escrito en Node.js y que nos proporciona una API para el estándar de W3C WebDriver API (quizá os suene más por el nombre de Selenium WebDriver).

Sus características principales son las siguientes:

  • Proporciona una API que recubre el servidor de Selenium, el cual se ejecuta en un proceso aislado. Se puede desactivar si Selenium se ejecuta desde otra máquina.
  • Tiene una sintaxis sencilla que nos permite escribir tests con rapidez.
  • Tiene soporte para selectores de tipo css y xpath.
  • Dispone de un runner bastante flexible que, entre otras funcionalidades, permite lanzar los tests por grupos, por etiquetas y en uno o varios navegadores en paralelo.
  • Es compatible con proveedores cloud de testing como SauceLabs y BrowserStack.
  • Genera informes en formato JUnit XML, que pueden incluirse dentro de servidores de integración continua como Jenkins y Teamcity.
  • Se puede ejecutar desde su propio runner o a través de task runners como Grunt o Gulp.
  • Tiene unas librerías de comandos y aserciones propias muy flexibles, las cuales pueden ser fácilmente extendidas por nosotros.
  • Está bien documentado y dispone de una comunidad activa. En el momento de escribir estas líneas, el proyecto en Github dispone de 7376 estrellas y 659 forks, lo cual para ser un framework no demasiado popular, no está nada mal.

A parte de todo lo anterior, su curva de aprendizaje es muy rápida y gracias a su API podemos cubrir la gran mayoría de casuísticas o necesidades que nos surjan.

En el otro lado tenemos a Cucumber-js, también escrito para Node.js, y cuyas características principales son:

  • Es un framework orientado a usarlo en ciclos de desarrollo con BDD.
  • Usa lenguaje Gherkin, que nos proporciona una sintaxis muy sencilla y hace que los criterios de aceptación sean:
    • Fáciles de leer.
    • Fáciles de entender.
    • Fáciles de generar debate.
    • Fáciles de convertir en una prueba automática.
  • Gherkin tiene soporte de multi-idioma (i18n).
  • Permite crear funciones hooks y usar tags dentro de nuestras pruebas.

Además, otro aspecto muy importante es que si conseguimos escribir nuestros criterios de aceptación en lenguaje Gherkin nos va a ayudar, ya de paso, a disponer de una documentación actualizada que detalla el comportamiento de la aplicación.

Y entonces… ¿qué hace Nightwatch-Cucumber? Fácil. Hace que ambos frameworks se integren correctamente. De ese modo, podemos disponer de las ventajas de ambos.

Preparando nuestro entorno

En primer lugar, debemos tener Node.js instalado en nuestro sistema. Para sistemas Windows, podemos descargar el instalador desde su página oficial. Sin embargo, para Mac OX o Linux recomiendo instalarlo con la herramienta NVM. También necesitaremos tener instalado git y el navegador Chrome.

Tened en cuenta que todos los comandos los he realizado sobre un sistema Linux (Ubuntu 17.10), si disponéis de un sistema Windows, podéis utilizar la consola que proporciona Git for Windows y que es compatible con un gran número de comandos Unix.

Como quiero mostraros la evolución de usar Nightwatch por separado a integrado con Cucumber, vamos a ver en estos primeros ejemplos cómo sería implementar nuestras pruebas solo en Nightwatch. Se ha subido al Github de Paradigma un repositorio con el código. Vamos a clonarlo.

$ git clone https://github.com/paradigmadigital/demo-nightwatch.git

Instalamos las dependencias de Node:

$ npm install

Esto nos instalará Nightwatch, Selenium Server y el driver de Chrome.

La estructura del proyecto es la que sigue:

El archivo más importante es nightwatch.conf.js, el cual contiene toda la configuración necesaria para la ejecución de Nightwatch. El directorio tests contiene todas nuestras pruebas. Este repositorio cuenta con tres ejemplos que veremos a continuación.

Ejecutando nuestro primer test

Pasamos a analizar el primer ejemplo. Nos abrimos el fichero: tests/example1/searchOnParadigmaSingleStep.js.

module.exports = {
  'Realizar una busqueda con el buscador de Paradigma Digital': function 
(client) {
    client
      .url('https://www.paradigmadigital.com')
      .maximizeWindow()
      .waitForElementVisible('#menu-item-topSearch', 5000)
      .assert.title('Paradigma - Transformación Digital Aplicada')
      .click('#menu-item-topSearch a')
      .waitForElementVisible('#menu-item-topSearch input[type=search]', 1000)
      .setValue('#menu-item-topSearch input[type=search]', 'Nightwatch')
      .assert.visible('#menu-item-topSearch input[type=submit]')
      .click('#menu-item-topSearch input[type=submit]')
      .waitForElementVisible('#searchPage', 5000)
      .click('#searchPage article a')
      .end()
  }
}

Este test básicamente hace lo siguiente:

  • Abre el navegador con la url de la página principal de Paradigma.
  • Introduce en el buscador la cadena de texto “Nightwatch” y realiza la búsqueda.
  • Hace clic sobre el primer post de la página de resultados.

Llegados a este punto, ya estamos en condiciones de ejecutar nuestra primera prueba. Al lanzar el test, veremos cómo se abre Chrome y se van llevando a cabo las acciones que le hemos definido como si las realizara un usuario. Solo que mucho más rápido.

$ npm run test – tests/example1/searchOnParadigmaSingleStep.js

La salida por consola debe ser algo como esto:

Si os fijáis, se va imprimiendo una traza con el resultado de cada búsqueda de elementos de la página (los selectores), así como los tiempos y el resultado de las comprobaciones de las aserciones.

Vale, está bien, aunque seguro que os ha llamado la atención lo “feo” y poco estructurado que está el código. A continuación veremos cómo en el ejemplo 2 mejoramos un poco las cosas.

Refactorizando un poco el código

Ahora abrimos el fichero: tests/example2/searchOnParadigmaMultiSteps.js.

module.exports = {

  before: function (client) {
    console.log('before: Abriendo el navegador')
    client
      .url('https://www.paradigmadigital.com')
      .maximizeWindow()
  },

  after: function (client) {
    console.log('after: Cerrando el navegador')
    client.end()
  },

  beforeEach: function () {
    console.log('beforeEach: Ejecutando step')
  },

  afterEach: function () {
    console.log('afterEach: Step ejecutado!')
  },

  '@tags': ['buscador', 'paradigma'],
  'Paso 1: Introducir la cadena de busqueda en el buscador': function (client) {
    client
      .waitForElementVisible('#menu-item-topSearch', 5000)
      .assert.title('Paradigma - Transformación Digital Aplicada')
      .click('#menu-item-topSearch a')
      .waitForElementVisible('#menu-item-topSearch input[type=search]', 1000)
      .setValue('#menu-item-topSearch input[type=search]', 'Nightwatch')
  },

  'Paso 2: Realizar la busqueda': function (client) {
    client
      .assert.visible('#menu-item-topSearch input[type=submit]')
      .click('#menu-item-topSearch input[type=submit]')
  },

  'Paso 3: Navegando al primer resultado de la pagina': function (client) {
    client
      .waitForElementVisible('#searchPage', 5000)
      .click('#searchPage article a')
  }

}

Se trata del mismo test que el anterior, pero un poco refactorizado. Como podemos observar, la ejecución del test se ha separado en varios pasos dándole a cada uno un título descriptivo.

Solo con esto ya mejoramos bastante la legibilidad y comprensión del código. Si ejecutamos este test veremos que la salida por consola también proporciona más detalle ayudándonos a comprender mejor lo que se está probando.

$ npm run test – tests/example2/searchOnParadigmaMultiSteps.js

Si os habéis fijado, en este segundo ejemplo también se han añadido varios tags (‘buscador’, ‘paradigma’, ‘ejemplo2’) para asignarle una cierta organización. Esto es realmente útil cuando disponemos de un gran número de tests y nos interesa lanzar solo uno o un conjunto sin necesidad de que se ejecute el resto. Por ejemplo, con el comando:

$ npm run test -- -a ejemplo2

Se lanzarían todos los etiquetados como “ejemplo2” y que en nuestro caso solo se corresponde con este mismo test.

Además de la estructura de ejecución por pasos y las etiquetas, se han añadido cuatro funciones hooks que se ejecutan en distintas etapas del test.

Las funciones beforeEach y afterEach se ejecutan respectivamente antes y después de cada paso. Sin embargo, before y after lo hacen antes y después de ejecutar el test. Esto nos permite reutilizar código que sino tendríamos repetidas veces, incluir funciones para logging, etc.

Sacándole partido a Nightwatch

Para finalizar con los ejemplos de este repositorio, pasamos a analizar el último de los ejemplos el cual se aprovecha de algunas de las otras características que Nightwatch nos proporciona.

Algunas de ellas son: variables globales, comandos y aserciones creados por nosotros y estructurar el código siguiendo el patrón de page object.

Si nos abrimos el fichero: tests/example3/searchOnYahoo.js veremos a lo que me refiero.

module.exports = {

  before: function (client) {
    console.log('before: Abriendo el navegador');
    client
      .page.yahoo.home.searchToolbar()
      .navigate();

    client.maximizeWindow();
  },

  after: function (client) {
    console.log('after: Cerrando el navegador');
    client.end();
  },

  beforeEach: function () {
    console.log('beforeEach: Ejecutando step');
  },

  afterEach: function () {
    console.log('afterEach: Step ejecutado!');
  },

  '@tags': ['buscador', 'yahoo'],
  "Paso 1: Introducir la cadena de busqueda en el buscador": function (client) {
    var yahooSearch = client.page.yahoo.home.searchToolbar();

    yahooSearch
      .waitForPageLoaded()
      .assert.title('Yahoo Search - Búsqueda en la Web')
      .setValue('@searchBar', 'Nightwatchjs org')
      .assert.hasFocus('@searchBar')
  },

  'Paso 2: Realizar la busqueda': function (client) {
    var yahooSearch = client.page.yahoo.home.searchToolbar();

    yahooSearch
      .assert.visible('@submit')
      .click('@submit');
  },

  'Paso 3: Navegar a la segunda pagina': function (client) {
    var yahooPagination = client.page.yahoo.searchPage.pagination();

    yahooPagination
      .waitForPageLoaded()
      .click('@nextPage');
  },

  'Paso 4: Hacer click sobre el cuarto resultado': function (client) {
    var yahooResults = client.page.yahoo.searchPage.searchResults();

    yahooResults
      .waitForElementVisible('@results', client.globals.timeout.long)
      .clickOnResult(4);
  }

}

De nuevo vemos que el test está separado en varios pasos, dispone de funciones after/before, pero la diferencia más notable es que el código se encapsula dentro de funciones que nos hemos creado en la carpeta pageObjects.

Esta técnica es muy beneficiosa, ya que podemos darle una estructura al código de nuestros tests sacando “factor común” por pantalla/lógica de aplicación y, así, conseguir una alta cohesión y hacer que el código sea más mantenible y reutilizable. Además también nos ayuda a que haya menos cosas “a fuego”.

Para muestra un botón. Si abrimos el fichero: pageObjects/yahoo/home/searchToolbar.js nos daremos cuenta de que encapsula la home del buscador de Yahoo!

Dentro de cualquier page object podemos definir nuestros propios comandos (en el ejemplo myCommands), url y selectores css (por defecto) y xpath e incluso podríamos dotarle de una estructura más lógica a nuestros selectores si hacemos uso de las secciones.

const myCommands = {
  waitForPageLoaded: function (timeout) {
    const myTimeout = timeout || this.api.globals.timeout.medium
    return this
      .waitForElementVisible('@searchBar', myTimeout)
      .waitForElementVisible('@submit', myTimeout)
  }
}

module.exports = {
  commands: [myCommands],
  url: 'https://es.search.yahoo.com/',
  elements: {
    searchBar: {
      selector: 'input[type=text]'
    },
    submit: {
      selector: '//*[@title="Search"]',
      locateStrategy: 'xpath'
    }
  }
}

Además, también podemos utilizar variables globales (en el ejemplo this.api.globals.timeout). En nuestro caso, nos hemos definido unos valores globales para controlar los timeouts.

Para invocar a este page object desde nuestro test basta con llamarlo así:

client.page.yahoo.home.searchToolbar().setValue('@searchBar', 'Nightwatchjs org')

La función setValue lo que está haciendo es introducir en la barra de búsqueda de la home de Yahoo (selector ‘@searchBar’) la cadena de texto ‘Nightwatchjs org’.

Os recomiendo que analicéis el resto de código vosotros mismos y los ficheros del directorio pageObjects y, ante cualquier duda o consulta, me pongáis un comentario en el post.

Integrando Nightwatch con Cucumber

Ahora vamos a ver cómo se integra Cucumber con Nightwatch. Tenéis disponible para clonarnos un segundo repositorio con ejemplos.

$ git clone https://github.com/paradigmadigital/demo-nightwatch-cucumber.git

Lo ejecutamos directamente y pasamos a analizar las diferencias.

$ npm run test

La salida por consola es algo como lo de a continuación:

Esto es otra cosa. Estoy seguro de que con solo ver la salida de consola cualquier persona podría llegar a entender lo que está haciendo.

Cucumber trabaja con ficheros .feature que están escritos en lenguaje Gherkin, como adelantamos al inicio. Para ello, se ha creado en este repositorio un directorio llamado features y en el que hemos dejado el fichero: features/searchOnYahoo.feature.

Si lo abrimos veremos la pinta que tiene:

# language: es

@yahoo
Característica: Busqueda en Yahoo

  Escenario: Abrir la pagina del buscador de Yahoo
    Dado que he navegado a la pagina del buscador de Yahoo
    Entonces el titulo de la pagina es "Yahoo Search - Búsqueda en la Web"
    Y puedo ver el campo de texto para realizar la busqueda

  @busqueda
  Escenario: Realizando una busqueda en el buscador
    Dado que he navegado a la pagina del buscador de Yahoo
    Cuando realizo una busqueda de texto con "Nightwatchjs org"
    Entonces se cargan los resultados
    Y el titulo de la pagina contiene el texto "Nightwatchjs org"

  @busqueda
  Escenario: Navegando al cuarto resultado de la segunda pagina de busqueda
    Dado que he realizado una busqueda de texto con "Nightwatchjs org"
    Cuando navego a la siguiente pagina de resultados
    Y hago click sobre el resultado 4
    Entonces navego a la pagina que he seleccionado

  @busqueda @multi
  Esquema del escenario: Introduciendo distintos textos en el buscador
    Dado que he navegado a la pagina del buscador de Yahoo
    Cuando realizo una busqueda de texto con "<text>"
    Entonces se cargan los resultados
    Y el titulo de la pagina contiene el texto "<text>"
    Ejemplos:
      | text                                   |
      | wikipedia                              |
      | Paradigma Digital                      |
      | Bob Esponja cantando We Will Rock You! |

Estos son todos nuestros casos (escenarios) a probar. Prácticamente son casi los mismos que en el ejemplo 3 con alguno más para mostraros diferentes usos de Gherkin (Escenario y Esquema del escenario).

Todos ellos se refieren a una misma funcionalidad que es la búsqueda de Yahoo. Para que Nightwatch-Cucumber sepa interpretar estos pasos, debemos incluir la implementación de cada uno de ellos dentro de un fichero javascript que alojaremos en el directorio features/stepDefinitions.

Por ejemplo, para estos escenarios se ha creado el fichero: features/stepDefinitions/yahooSteps.js.

const {defineSupportCode} = require('cucumber')

defineSupportCode(({Given, Then, When}) => {
  Given(/^que he navegado a la pagina del buscador de Yahoo$/, () => {
    const yahooSearch = client.page.yahoo.home.searchToolbar()
    return yahooSearch
      .navigate()
      .waitForPageLoaded()
  })

  Given(/^que he realizado una busqueda de texto con "([^"]*)"$/, (text) => {
    const yahooSearch = client.page.yahoo.home.searchToolbar()
    return yahooSearch
      .navigate()
      .waitForPageLoaded()
      .setValue('@searchBar', text)
      .verify.hasFocus('@searchBar')
      .assert.visible('@submit')
      .click('@submit')
  })

  When(/^realizo una busqueda de texto con "([^"]*)"$/, (text) => {
    const yahooSearch = client.page.yahoo.home.searchToolbar()
    return yahooSearch
      .setValue('@searchBar', text)
      .verify.hasFocus('@searchBar')
      .assert.visible('@submit')
      .click('@submit')
  })

  When(/^navego a la siguiente pagina de resultados$/, () => {
    const yahooPagination = client.page.yahoo.searchPage.pagination()
    return yahooPagination
      .waitForPageLoaded()
      .click('@nextPage')
  })

  When(/^hago click sobre el resultado (\d)$/, (position) => {
    const yahooResults = client.page.yahoo.searchPage.searchResults()
    return yahooResults
      .waitForPageLoaded(client.globals.timeout.short)
      .clickOnResult(position)
  })

  Then(/^el titulo de la pagina es "([^"]*)"$/, (title) => {
    return client.assert.title(title)
  })

  Then(/^puedo ver el campo de texto para realizar la busqueda$/, () => {
    const yahooSearch = client.page.yahoo.home.searchToolbar()
    return yahooSearch.assert.visible('@searchBar')
  })

  Then(/^se cargan los resultados$/, () => {
    const timeout = client.globals.timeout.long
    const yahooResults = client.page.yahoo.searchPage.searchResults()
    return yahooResults.waitForPageLoaded(timeout)
  })

  Then(/^el titulo de la pagina contiene el texto "([^"]*)"$/, (text) => {
    return client.getTitle((title) => {
      client.assert.ok(title.includes(text))
    })
  })

  Then(/^navego a la pagina que he seleccionado$/, () => {
    // El buscador de yahoo abre una nueva pestaña al hacer click
    // por eso hay que cambiar el foco a la pestaña nueva
    client.window_handles((result) => {
      const handle = result.value[1]
      client.switchWindow(handle)
    })

    // Aseguramos que el titulo de la pestaña no contiene la cadena "Yahoo"
    //
    // Nota: si conociesemos cual es la pagina del listado de resultados
    // (y su posicion no cambiara) habria que verificarlo de otro modo,
    // por ejemplo, buscando un elemento que sepamos que existe en la pagina
    client.getTitle((title) => {
      // Como ya tenemos el titulo, podemos cerrar la pestaña
      client.closeWindow()
      // Comprobamos que el titulo no contenga "Yahoo"
      client.assert.ok(!title.includes('Yahoo'))
    })

    // Nos movemos de nuevo a la pestaña inicial (0 = primera posicion)
    return client.window_handles((result) => {
      const handle = result.value[0]
      client.switchWindow(handle)
    })
  })

})

Básicamente de lo que se trata es de definir el tipo de step:

  • Given: Son los Dado, las precondiciones de nuestra prueba.
  • When: Equivale a los Cuando, acciones del usuario o eventos que ocurren.
  • Then: Equivale a los Entonces, los resultados que esperamos.

A continuación, se captura con una expresión regular el texto de cada paso y dentro de la función se implementa su comportamiento. Por supuesto, y aquí es donde está la clave, podemos seguir utilizando todas las características que Nightwatch nos proporciona: page objects, globals, comandos, aserciones, etc.

Y no solo eso. También seguimos teniendo disponibles prácticamente todas las características del runner de Nightwatch, pero ya orientado a trabajar con features.

Por ejemplo, podemos seguir lanzando tests según su tag, la diferencia es que ese tag se pone como una anotación (‘@busqueda’) dentro del fichero .feature. El feature pasa a ser lo que antes era nuestro test en Nightwatch. Por ejemplo, si ejecutamos:

$ npm run test -- -a yahoo
$ npm run test -- -a busqueda
$ npm run test -- -a multi

Se lanzarían todos los escenarios (la feature completa), solo los escenarios 2, 3 y 4, y solo el escenario 4 respectivamente.

Generando informes en HTML

Otra de las ventajas de utilizar este framework es que al finalizar la ejecución de los escenarios escribe los resultados en un fichero json dentro de la carpeta reports. Ese json puede ser interpretado por la librería de cucumber-html-reporter, que genera un informe en HTML. Si ejecutamos el siguiente comando:

$ npm run reporter

Nos abrirá una página web con el informe de la ejecución de nuestros escenarios.

Si alguno de ellos falla, nos adjunta el mensaje de error y una captura de pantalla justo en el punto en el que se produjo el fallo:

Como adelantaba al principio del post, estos informes pueden ser incluidos dentro de nuestro servidor de integración continua. De este modo, podemos tener un feedback muy rápido y sobre todo visual del estado de la ejecución de nuestras pruebas.

Conclusión

Hemos visto un modo de implementar pruebas de aceptación y end-to-end con dos de los frameworks que existen para Node.js que mejor funcionan de forma conjunta y que nos pueden ayudar a realizar este tipo de pruebas.

La elección de las librerías de pruebas debe hacerse teniendo en cuenta muchos factores (lenguaje, características, documentación, etc.), y siempre debería ser una decisión con la que el equipo se sienta cómodo.

Si lo que habéis visto os ha resultado interesante, os animo a que reviséis el resto de código y realicéis vuestras pruebas para intentar sacarle el máximo partido. Luego quién sabe, quizá a partir de ahora Nightwatch-Cucumber pase a resultaros una opción interesante y a tener en cuenta en vuestros futuros desarrollos de pruebas.

Soy un perfeccionista de libro y cuando todo funciona como un engranaje, siento como un cosquilleo. Trabajo en el departamento de QA asegurando que los productos digitales cumplen con la calidad además de las necesidades y expectativas del cliente. Quizás también me encuentres desarrollando, analizando o cacharreando con cualquier parte de código. También me gusta la música electrónica y hacer como que toco el piano.

Ver toda la actividad de Víctor J. Díaz Ayuste

Escribe un comentario