En Mi Local Funciona

Technical thoughts, stories and ideas

Automatización de pruebas con Puppeteer: ¿El fin de la era Selenium?

Publicado por Eduardo Riol el

QAPuppeteer

Si hay una herramienta que a todos nos viene a la cabeza cuando hablamos de automatización de pruebas funcionales contra la interfaz gráfica en el mundo web, ésa es Selenium WebDriver. Tradicionalmente en el mundo empresarial han destacado otras soluciones propietarias como UFT o Silk Test (ambas de Microfocus tras la compra de las plataformas a HP y Borland, respectivamente). Sin embargo es innegable que en los últimos años Selenium, dada su condición de proyecto de código abierto y el alto grado de madurez que ha alcanzado, se ha posicionado como la solución de referencia a la hora de afrontar automatizaciones funcionales, reduciendo costes de licenciamiento y favoreciendo una alta integración con diversos stacks tecnológicos (.NET, Python, Node.js). Y es que Selenium ya no es sólo una solución para el mundo Java.

Sin embargo de un tiempo a esta parte ha empezado a sonar con mucha fuerza Puppeteer, que está desarrollada por Google y tiene el apoyo del gigante americano, con todo lo que ello supone. Puppeteer es una librería de Node.js que proporciona una API de alto nivel que permite automatizar acciones sobre los navegadores de Google: tanto Chrome como su versión de código abierto Chromium.

¿Pero por qué me voy a cambiar a Puppeteer?

La primera pregunta que debemos hacernos es, más allá del glamour que aporta el ser un proyecto esponsorizado por Google, qué motivos nos deben hacer plantearnos el usar Puppeteer. Algunas de las ventajas más notables que proclama son las siguientes:

  • Permite acceder a la medición de tiempos de carga y renderizado que proporciona la herramienta de Performance Analysis de Chrome.
  • Permite un mayor control sobre Chrome que el que ofrece Selenium WebDriver.
  • No necesita referenciar a un driver externo para ejecutar los tests (chromedriver.exe en el caso de Chrome para Selenium WebDriver), si bien este problema en Selenium está mitigado gracias a dependencias como el WebDriverManager de Boni García.

Además, aunque no es una ventaja propiamente dicha, Puppeteer se caracteriza por ofrecer por defecto una ejecución de las pruebas en modo headless, pudiéndose configurar para usar en modo no-headless.

Por supuesto, como los lectores más avezados están ya vislumbrando, existen algunas desventajas respecto a Selenium, siendo las principales que Puppeteer no permite automatizar pruebas en navegadores que no sean Chrome o Chromium, y que actualmente sólo permite su uso en JavaScript para Node.js.

Manos a la obra: un ejemplo con Puppeteer

En cualquier caso, más allá de las ventajas que puedan proclamar sus desarrolladores, no hay nada como ver un ejemplo de Puppeteer en acción para empezar a familiarizarnos con él. Tenéis el código fuente del ejemplo en mi cuenta de GitHub.

Previamente a la implementación de dicho ejemplo, tal y como se ve en el README del repositorio, es necesario disponer de Node.js y Chrome instalados en vuestro equipo. También en dicho fichero README podéis ver las instrucciones de ejecución de las pruebas. En este post me centraré en explicar el código.

Como ocurre con Selenium WebDriver en el mundo Java, donde para la definición de las pruebas debemos apoyarnos en un framework de testing como JUnit, en el caso de Puppeteer emplearemos Mocha, un framework de testing para proyectos JavaScript que se ejecuta sobre Node.js, y Chai, una librería que nos permite hacer uso de asserts amigables, al más puro estilo de Hamcrest en Java.

La idea del ejemplo es automatizar tres sencillas pruebas sobre el buscador DuckDuckGo. En concreto comprobaremos el título de la página, haremos una búsqueda comprobando que se muestran resultados, y finalmente buscaremos un término inexistente para comprobar que no se muestra ningún resultado. La implementación de dichas pruebas sería así:

const puppeteer = require('puppeteer');
const { expect }  = require('chai');

describe('Duck Duck Go search using basic Puppeteer', () => {

    let browser;
    let page;

    beforeEach(async () => {
        browser = await puppeteer.launch();
        page = await browser.newPage();
        await page.goto('https://duckduckgo.com');
    });

    afterEach(async () => {
        await browser.close();
    });

    it('should have the correct page title', async () => {
        expect(await page.title()).to.eql('DuckDuckGo — Privacy, simplified.');
    });

    it('should show a list of results when searching actual word', async () => {
        await page.type('input[id=search_form_input_homepage]', 'puppeteer');
        await page.click('input[type="submit"]');
        await page.waitForSelector('h2 a');
        const links = await page.evaluate(() => {
            return Array.from(document.querySelectorAll('h2 a'));
        });
        expect(links.length).to.be.greaterThan(0);
    });

    it('should show a warning when searching fake word', async () => {
        await page.type('input[id=search_form_input_homepage]', 'pupuppeppeteerteer');
        await page.click('input[type="submit"]');
        await page.waitForSelector('div[class=msg__wrap]');
        const text = await page.evaluate(() => {
            return document.querySelector('div[class=msg__wrap]').textContent;
        });
        expect(text).to.contain('Not many results contain');
    });

});

Como vemos, la API de Puppeteer para interactuar con Chrome es bastante intuitiva, haciendo uso de funciones como las siguientes:

  • puppeteer.launch(): para inicializar el navegador Chrome.
  • browser.newPage(): para crear una nueva página en el contexto del navegador inicializado.
  • page.goTo(url): para navegar a una página determinada.

Y así un largo etcétera de funciones que podemos ver al completo en la descripción de la API en GitHub.

Al igual que ocurre en Java con JUnit, aprovechamos el framework Mocha para definir pasos previos o posteriores a las pruebas con beforeEach y afterEach, respectivamente.

En el ejemplo he usado las instrucciones async/await de JavaScript para la definición de las pruebas con el objetivo de acercarlo más al paradigma síncrono de las pruebas en Java, para la gente que viene de Selenium WebDriver. Sin embargo, si te sientes ducho en JavaScript, debes saber que Puppeteer también permite hacer uso de las promesas, como vemos en este ejemplo de uso de la librería.

Este ejemplo muestra bastante a las claras un uso básico de Puppeteer, pero... ¿Realmente esta sintaxis de automatización es escalable y mantenible?

Mejorando el ejemplo: Page Object Mode

Cualquier persona habituada a automatizar pruebas sabe que uno de los factores críticos a tener en cuenta es el uso de patrones de programación que permitan tener un juego de pruebas más mantenible y con los datos que más cambian (generalmente la definición de los elementos de la interfaz), centralizados en un único lugar. Es por ello que necesitamos patrones como Page Object Model (POM), que permiten estructurar nuestro código de pruebas en base a la estructura de la web que queremos automatizar, representando cada página y los objetos que contiene como un único artefacto que expone su propia API para que nuestros tests puedan solicitar determinadas acciones.

Sin embargo, ¿es posible aplicar este paradigma en JavaScript? La respuesta es SÍ. Podemos hacer uso de las clases de JavaScript para establecer una estructura de páginas con sus funciones y hacer que nuestras pruebas sean más mantenibles y fácilmente inteligibles.

Haciendo uso de POM, podemos definir una clase HomePage que representa la página inicial del buscador DuckDuckGo, de la siguiente forma:

class HomePage {

    constructor(page) {
        this.page = page;
    }

    async getTitle() {
        return this.page.title();
    }

    async searchFor(word) {
        await this.page.type('input[id=search_form_input_homepage]', word);
        await this.page.click('input[type="submit"]');
    }

}

module.exports = HomePage;

Además, la página de resultados también podemos adaptarla al patrón POM definiendo una clase ResultsPage, con el siguiente código:

class ResultsPage {

    constructor(page) {
        this.page = page;
    }

    async getNumberOfLinks(page) {
        await this.page.waitForSelector('h2 a');
        const links = await this.page.evaluate(() => {
            return Array.from(document.querySelectorAll('h2 a'));
        });
        return links.length;
    }

    async checkIfResultsExist(page) {
        await this.page.waitForSelector('div[class=msg__wrap]');
        const text = await this.page.evaluate(() => {
            return document.querySelector('div[class=msg__wrap]').textContent;
        });
        return !text.includes('Not many results contain');
    }

}

module.exports = ResultsPage;

De esta forma, la definición de nuestros tests se simplifica y se hace más fácilmente inteligible, abstrayendo a la persona que lea los tests de los detalles de implementación de los mismos a bajo nivel:

const puppeteer = require('puppeteer');
const { expect }  = require('chai');
const HomePage = require('./pages/homePage');
const ResultsPage = require('./pages/resultsPage');

describe('Duck Duck Go search using Puppeteer with Page Object Model', () => {

    let browser;
    let page;

    beforeEach(async () => {
        browser = await puppeteer.launch();
        page = await browser.newPage();
        await page.goto('https://duckduckgo.com');
    });

    afterEach(async () => {
        await browser.close();
    });

    it('should have the correct page title', async () => {
        const homePage = new HomePage(page);
        expect(await homePage.getTitle()).to.eql('DuckDuckGo — Privacy, simplified.');
    });

    it('should show a list of results when searching actual word', async () => {
        const homePage = new HomePage(page);
        await homePage.searchFor('puppeteer');
        const resultsPage = new ResultsPage(page);
        expect(await resultsPage.getNumberOfLinks()).to.be.greaterThan(0);
    });

    it('should show a warning when searching fake word', async () => {
        const homePage = new HomePage(page);
        await homePage.searchFor('pupuppeppeteerteer');
        const resultsPage = new ResultsPage(page);
        expect(await resultsPage.checkIfResultsExist()).to.be.false;
    });

});

Concluyendo, que es gerundio

En mi opinión Puppeteer es una herramienta muy interesante que puede empezar a robar gran parte de protagonismo a Selenium WebDriver en contextos en los que no se requiera la automatización de pruebas en diferentes navegadores, así como en equipos acostumbrados al trabajo en stack Node.js. Igualmente, es una librería muy potente si queremos leer valores de carga y rendimiento de las webs como parte de las comprobaciones a realizar en nuestros tests.

Sin embargo, esas limitaciones en cuanto a la compatibilidad entre diferentes navegadores y la existencia de Puppeteer en un único lenguaje, hacen que por hoy en mi opinión el predominio de Selenium no se vaya a ver amenazado.

Como desarrolladores y testers, será interesante contar con Puppeteer como un recurso más con el que contar en nuestra caja de herramientas.

Y es que, como he leído en algún comentario en Hacker News: "I know plenty of people who dislike working with Selenium. I haven't yet met anyone who's had enough experience with Puppeteer to hate it yet."

Si te gustado, ¡síguenos en Twitter para estar al día de nuevos posts!

Eduardo Riol
Autor

Eduardo Riol

Líder Técnico de QA & Testing en atSistemas. Mis intereses se centran en el control de la deuda técnica, BDD y la integración de QA en entornos Agile y DevOps. Sígueme en Twitter: @eduriol