E2E testing con Travis,NightWatch y SauceLabs

Repo de ejemplo: https://github.com/IagoLast/e2e-travis-saucelabs

Cuando estoy programando un side-project en lo último que pienso es en los navegadores en los que va a funcionar mi código. Normalmente ya es un milagro que funcione en mi máquina! Pero en el mundo real si queremos ser medianamente serios lo mínimo que deberíamos dar es una lista de navegadores compatibles con nuestra app o librería.

En este artículo voy a hablar sobre como crear un entorno de integración continua con tests e2e utilizando Travis, Nightwatch y SauceLabs

Aplicación de ejemplo.

La aplicación que vamos a probar, es una web-app que simplemente muestra Hello World utilizando características de es6 como const o template literals.

Por simplicidad toda mi aplicación cabe en un simple index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

</head>

<body>
    <script>
        const name = 'WORLD';
        const helloElement = document.createElement('h1');
        helloElement.innerText = `Hello ${name}`;
        document.body.appendChild(helloElement);
    </script>
</body>

</html>

Como se puede ver lo único que hace el esta “app” pintar un h1 con el contenido Hello WORLD a el body.

Sirviendo la app en local

serve es una librería de node que permite crear un servidor web facilmente para probar una web en local.

Se puede instalar escribiendo:

yarn add --dev serve

Con el comando serve se crea un servidor web que sirve el index.html de mi web-app en el puerto 5000.


 $(npm bin)/serve

   ┌──────────────────────────────────────────────────┐
   │                                                  │
   │   Serving!                                       │
   │                                                  │
   │   - Local:            http://localhost:5000      │
   │   - On Your Network:  http://192.168.0.13:5000   │
   │                                                  │
   │   Copied local address to clipboard!             │
   │                                                  │
   └──────────────────────────────────────────────────┘

Efectivamente, abriendo chrome en http://localhost:5000 puedo ver mi aplicación funcionando correctamente

Configurando Nightwatch.js

Lo habitual es que por cada cambio que haga en mi app, vuelva a abrir el navegador y comprobar a mano que todo funciona correctamente, Nightwatch.js es una herramienta que permite automatizar este proceso de forma que podemos dedicar ese tiempo a otras cosas más útiles.

Para instalar nightwatch basta con escribir

    yarn add --dev nightwatch

Para ejecutar nightwatch hace falta un archivo de configuración nightwatch.conf.js:

// nightwatch.conf.js

module.exports = {
    src_folders: ['test'], // Array de carpetas donde se encuentran los tests
    test_settings: {
        default: {
            desiredCapabilities: {
                browserName: 'chrome', // Navegador que va a ser controlado
            }
        },
    }
};

y una carpeta donde se encuentran los tests, en este caso solamente hay un test test/basic.js:

// test/basic.js

module.exports = {
  basicTest: function (browser) { // Define a simple test
    // Navigate to web-app url
    browser.url('http://localhost:5000'); 
    // Wait the page to be loaded
    browser.waitForElementVisible('body', 1000);
    // Expect to have a h1 element
    browser.expect.element('h1').to.be.present;
    // Expect the element to have text Hello WORLD
    browser.expect.element('h1').text.to.equal('Hello WORLD');
    // Finish browser session.
    browser.end();
  },
};

Este test navega a la url donde se esta siriviendo la webapp, espera a que el contenido se cargue y comprueba que el h1esta presente con el tenxto Hello WORLD.

Para ejecutar las pruebas en local hace falta tener un servidor de selenium corriendo junto con un Browser Driver.

En mac se puede instalar selenium mediante:

brew install selenium-server-standalone

Y el driver de chrome:

brew install chromedriver

Para ejecutar selenium basta con escribir:

selenium-server

Y con esto ya podemos ejecutar nightwatch, que controlará nuestro navegador a traves del browser driver que tengamos instalado y ejecutará los tests de forma automática.

MacBook-Pro-de-CARTO:e2e-travis-saucelabs iago$ $(npm bin)/nightwatch

[Basic] Test Suite
======================

Running:  basicTest
 ✔ Element <body> was visible after 45 milliseconds.
 ✔ Expected element <h1> to be present
 ✔ Expected element <h1> text to equal: "Hello WORLD"

OK. 3 assertions passed. (1.363s)

Como era de esperar los tests pasan.

Sauce Labs

Sauce labs es una herramienta gratis para proyectos open source que nos permite ejecutar pruebas de selenium contra diferentes plataformas de forma moderadamente sencilla.

Configurando usuario y contraseña

Para utilizar sauce-labs tendremos que crearnos una cuenta y obtener nuestro nombre de usuario y access-key. Una vez las tengamos tendremos que editar nuestro archivo de configuración de nightwatch añadiendo el puerto, el host y nuestro usuario y access-key.

Para NO subir claves secretas a github guardo mis variables de entorno en un archivo .env y las cargo mediante dotenv

require('dotenv').config(); // Carga la informacion secreta.

module.exports = {
    src_folders: ['test'], // Array de carpetas donde se encuentran los tests
    test_settings: {
        default: {
            desiredCapabilities: {
                browserName: 'chrome', // Navegador que va a ser controlado
            },
            selenium_port: 80, // Puerto en el que sauce sirve selenium
            selenium_host: 'ondemand.saucelabs.com', // Url de saucelabs
            username: process.env.SAUCE_USERNAME, // Nombre de usuario de saucelabs
            access_key: process.env.SAUCE_ACCESS_KEY, // Api key de sauce labs
        },
    }
};

sauce-connect

Como nuestro servidor web esta en nuestra máquina local y no es accesible desde el exterior, necesitamos comunicar los servidores de sauce con nuestra web-app, para ello utilizaremos sauce connect, hay que bajarse el binario y ejecutarlo pasandole como parametros usuario y api-key

bin/sc -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>

Si se ha ejecutado correctamente veremos que hay un tunel activo en el dashboard de sauce.

Si ejecutamos de nuevo nightwatch este se ejecutará contra los servidores de sauce.


$(npm bin)/nightwatch

[Basic] Test Suite
======================

Running:  basicTest
 ✔ Element <body> was visible after 1013 milliseconds.
 ✔ Expected element <h1> to be present
 ✔ Expected element <h1> text to equal: "Hello WORLD"

OK. 3 assertions passed. (6.88s)


Configurando diferentes navegadores

La gracia de todo esto es que con unos pequeños cambios en el archivo de configuracion de nightwatch, podemos ejecutar nuestras pruebas contra diferentes navegadores, en este caso:

  • Firefox55
  • Internet explorer 11
  • MS Edge 15
require('dotenv').config(); // Carga la informacion secreta.

module.exports = {
    src_folders: ['test'], // Array de carpetas donde se encuentran los tests
    test_settings: {
        default: {
            desiredCapabilities: {
                browserName: 'chrome', // Navegador que va a ser controlado
            },
            selenium_port: 80, // Puerto en el que sauce sirve selenium
            selenium_host: 'ondemand.saucelabs.com', // Url de saucelabs
            username: process.env.SAUCE_USERNAME, // Nombre de usuario de saucelabs
            access_key: process.env.SAUCE_ACCESS_KEY, // Api key de sauce labs
        },
        // Añadimos diferentes navegadores para probar
        firefox55: {
            desiredCapabilities: {
                browserName: 'firefox',
                version: 55,
            }
        },
        ie11: {
            desiredCapabilities: {
                browserName: 'internet explorer',
                version: 11
            }
        },
        edge15: {
            desiredCapabilities: {
                browserName: 'MicrosoftEdge',
                version: 15,
            }
        },
    }
};

Para ejecutar las pruebas, debemos pasarle el nombre de los entornos al parametro env de nightwatch

$(npm bin)/nightwatch --env default,ie11,edge15,firefox55
Started child process for: default environment 
Started child process for: ie11 environment 
Started child process for: edge15 environment 
Started child process for: firefox55 environment 

  >> default environment finished.  


  >> firefox55 environment finished.  


  >> ie11 environment finished.  


  >> edge15 environment finished.  

 default   [Basic] Test Suite
======================
 default   
 default   Results for:  basicTest
 default   ✔ Element <body> was visible after 1065 milliseconds.
 default   ✔ Expected element <h1> to be present
 default   ✔ Expected element <h1> text to equal: "Hello WORLD"
 default   OK. 3 assertions passed. (7.634s)
 default   
 ie11   [Basic] Test Suite
======================
 ie11   
 ie11   Results for:  basicTest
 ie11   ✔ Element <body> was visible after 1094 milliseconds.
 ie11   ✖ Expected element <h1> to be present - element was not found  - expected "present" but got: "not present"
 ie11       at Object.basicTest (/Users/iago/Workspace/personal/e2e-travis-saucelabs/test/basic.js:8:20)
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
 ie11   FAILED:  1 assertions failed and 1 passed (17.289s)
 ie11   
 ie11    _________________________________________________
 ie11   TEST FAILURE:  1 assertions failed, 1 passed. (17.416s)
 ie11    ✖ basic
 ie11   - basicTest (17.289s)
 ie11      Expected element <h1> to be present - element was not found  - expected "present" but got: "not present"
 ie11          at Object.basicTest (/Users/iago/Workspace/personal/e2e-travis-saucelabs/test/basic.js:8:20)
       at _combinedTickCallback (internal/process/next_tick.js:67:7)
 ie11   
 edge15   [Basic] Test Suite
======================
 edge15   
 edge15   Results for:  basicTest
 edge15   ✔ Element <body> was visible after 1183 milliseconds.
 edge15   ✔ Expected element <h1> to be present
 edge15   ✔ Expected element <h1> text to equal: "Hello WORLD"
 edge15   OK. 3 assertions passed. (20.673s)
 edge15   
 firefox55   [Basic] Test Suite
======================
 firefox55   
 firefox55   Results for:  basicTest
 firefox55   ✔ Element <body> was visible after 1039 milliseconds.
 firefox55   ✔ Expected element <h1> to be present
 firefox55   ✔ Expected element <h1> text to equal: "Hello WORLD"
 firefox55   OK. 3 assertions passed. (11.596s)
 firefox55   

En menos de 12 segundos, hemos probado nuestra app en 4 navegadores diferentes y internet explorer 10 ha fallado por que entre otras cosas no tiene soporte para los string literals que utilizamos en nuestra web-app.

Notificando a sauce el resultado

En el dashboard de sauce labs podemos ver nuestros tests en los 4 navegadores sin embargo no tenemos ningun tipo de feedback acerca de su han sido exitosos o no.

Se puede utilizar la REST API de saucelabs para actualizar la información de los jobs (tests) desde nightwatch. Para ello vamos a crear un pequeño snippet llamado sauce-feedback.js

var request = require('request');

function uploadSauceResults(browser, done) {
  // Finish browser session;
  browser.end();

  var user = browser.options.username;
  var key = browser.options.accessKey;
  var jobId = browser.sessionId;
  var passed = browser.currentTest.results.failed === 0;

  if (user && key && jobId) {
    var url = 'https://saucelabs.com/rest/v1/' + user + '/jobs/' + jobId;
    return request.put({
      url: url,
      auth: { username: user, password: key },
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ passed: passed })
    }, done);
  } else {
    console.warn('No user/key/jobId provided.');
    done();
  }
}

module.exports = uploadSauceResults;

Y lo vamos a utilizar en nuestro basic.js test.

// basic.js
const after = require('./sauce-feedback');
module.exports = {
    basicTest: function (browser) { // Define a simple test
        // Navigate to web-app url
        browser.url('http://localhost:5000');
        // Wait the page to be loaded
        browser.waitForElementVisible('body', 1000);
        // Expect to have a h1 element
        browser.expect.element('h1').to.be.present;
        // Expect the element to have text Hello WORLD
        browser.expect.element('h1').text.to.equal('Hello WORLD');
    },
    after: after,
};

Si vemos ahora el dashboard despues de ejecutar los tests, observamos los ticks verdes. (Por algun motivo que desconozco IE se muestra como completado en lugar de “error”, pero algo es algo! )

Integración con Travis.

Sería genial poder correr estos tests en nuestro sistema de integración continua, para probar de forma automática cada PR/commit.

Para ello crearemos un archivo travis.yml

language: node_js

node_js:
  - 8

cache: yarn

addons:
  sauce_connect: true

before_script:
  - yarn serve &

Por defecto travis utiliza los npm scripts para la CI por lo que habra que actualizar el package.json y añadir dos comandos, test y serve

// package.json

"scripts": {
    "test": "nightwatch --env default,ie11,edge15,firefox55",
    "serve": "serve"
}

El addon sauce_connect requiere que esten definidas en travis las variables de entorno para el SAUCE_ USERNAME y el SAUCE_ACCESS_KEY la forma más sencilla es definirlas en el cliente web

También tenemos que actualizar el nighwatch.conf.js con la informacion del build y el tunel-id.

// nightwatch.conf.js
const TRAVIS_JOB_NUMBER = process.env.TRAVIS_JOB_NUMBER; // Variable de entorno definida automaticamente por travis
require('dotenv').config();

module.exports = {
    src_folders: ['test'],
    test_settings: {
        default: {
            desiredCapabilities: {
                browserName: 'chrome',
                build: `build-${TRAVIS_JOB_NUMBER}`, // <----- importante para travis
                'tunnel-identifier': TRAVIS_JOB_NUMBER, // <----- importante para travis
            },
            selenium_port: 80,
            selenium_host: 'ondemand.saucelabs.com',
            username: process.env.SAUCE_USERNAME,
            access_key: process.env.SAUCE_ACCESS_KEY,
        },
        firefox55: {
            desiredCapabilities: {
                browserName: 'firefox',
                version: 55,
            }
        },
        ie11: {
            desiredCapabilities: {
                browserName: 'internet explorer',
                version: 11
            }
        },
        edge15: {
            desiredCapabilities: {
                browserName: 'MicrosoftEdge',
                version: 15,
            }
        },
    }
};

Con esto travis probara automaticamente cada commit o PR de la rama maste

En este caso nos avisa de que los tests están fallando en IE10 por lo que voy a reescribir la webapp para que funcione en todos los navegadores.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

</head>

<body>
    <script>
        var name = 'WORLD'; // No se usa const
        var helloElement = document.createElement('h1');
        helloElement.innerText = 'Hello ' + name; // No se usan string literals!
        document.body.appendChild(helloElement);
    </script>
</body>

</html>

Tras estos cambios vuelvo a ejecutar mis pruebas en local:

 yarn test
yarn run v1.2.1
$ nightwatch --env default,ie11,edge15,firefox55
Started child process for: default environment 
Started child process for: ie11 environment 
Started child process for: edge15 environment 
Started child process for: firefox55 environment 

  >> default environment finished.  


  >> firefox55 environment finished.  


  >> ie11 environment finished.  


  >> edge15 environment finished.  

 default   [Basic] Test Suite
======================
 default   
 default   Results for:  basicTest
 default   ✔ Element <body> was visible after 1009 milliseconds.
 default   ✔ Expected element <h1> to be present
 default   ✔ Expected element <h1> text to equal: "Hello WORLD"
 default   OK. 3 assertions passed. (7.259s)
 default   [Sauce Feedback] Test Suite
===============================
 default   
 default   OK. 3  total assertions passed. (8.641s)
 ie11   [Basic] Test Suite
======================
 ie11   
 ie11   Results for:  basicTest
 ie11   ✔ Element <body> was visible after 1086 milliseconds.
 ie11   ✔ Expected element <h1> to be present
 ie11   ✔ Expected element <h1> text to equal: "Hello WORLD"
 ie11   OK. 3 assertions passed. (13.08s)
 ie11   [Sauce Feedback] Test Suite
===============================
 ie11   
 ie11   OK. 3  total assertions passed. (14.456s)
 edge15   [Basic] Test Suite
======================
 edge15   
 edge15   Results for:  basicTest
 edge15   ✔ Element <body> was visible after 1013 milliseconds.
 edge15   ✔ Expected element <h1> to be present
 edge15   ✔ Expected element <h1> text to equal: "Hello WORLD"
 edge15   OK. 3 assertions passed. (20.44s)
 edge15   [Sauce Feedback] Test Suite
===============================
 edge15   
 edge15   OK. 3  total assertions passed. (21.878s)
 firefox55   [Basic] Test Suite
======================
 firefox55   
 firefox55   Results for:  basicTest
 firefox55   ✔ Element <body> was visible after 1124 milliseconds.
 firefox55   ✔ Expected element <h1> to be present
 firefox55   ✔ Expected element <h1> text to equal: "Hello WORLD"
 firefox55   OK. 3 assertions passed. (11.573s)
 firefox55   [Sauce Feedback] Test Suite
===============================
 firefox55   
 firefox55   OK. 3  total assertions passed. (12.949s)
✨  Done in 22.57s.

Y tal y como esperabamos, la web app funciona perfectamente en los 4 navegadores.

Resumen

Hemos montado un entorno que nos permite probar aplicaciones web de forma automática en múltiples navegadores. SauceLabs nos permite también elegir las plataformas sobre las que corren esos navegadores (windows, osx) e incluso permite dispositivos móviles.

Ese entorno se puede configurar junto a travis para probar de forma automática cada commit o Pull request de forma individual.

Referencias