Version curso.2024. Joaquín Cañadas <jjcanada@ual.es>

Objetivos
  • Exportar los test de Selenium IDE a Selenium Webdriver y ejecutar en Eclipse con distintos navegadores web (cross-browser testing).

  • Ejecutar los test de Selenium utilizando el navegador en modo headless.

Realización y entrega

La realización de estas actividades se realizará en equipo. La entrega será mediante el envío de un informe y el acceso al profesor a los servicios configurados, para la revisión y evaluación de los mismos.

1. Selenium WebDriver

Selenium WebDriver permite ejecutar los tests de Selenium como tests de JUnit, ofreciendo así la forma de ser ejecutados en Eclipse y Jenkins.

1.1. Exportar de Selenium IDE a JUnit

Exporta a formato JUnit los tests grabados con Selenium IDE. Si has organizado tus tests en test suites, podrás exportar cada test suite a una clase .java en donde cada test case será un método de test de la clase. Si no, tendrás que hacerlo cada test case uno por uno, ya que por ahora Selenium IDE no permite exportarlos todos a la vez.

selenium ide export test
Figura 1. Exportar test case
selenium ide export test junit
Figura 2. Exportar formato JUnit

Al exportar, marca las dos opciones que permiten que se guarde como comentarios el comando de Selenium IDE que da origen a cada instrucción de WebDriver. Guarda los archivos .java en una carpeta cualquiera, los necesitarás más adelante. A continuación los importaremos en Eclipse.

1.2. Creación del proyecto en Eclipse

En el momento de escribir este documento, Selenium IDE exporta los tests en formato JUnit 4. Veamos a continuación los pasos necesarios para su ejecución.

  1. En Eclipse, crea un nuevo proyecto Maven. Selecciona la opción Create a simple project (skip archetype selection)

eclipse new maven project
Figura 3. Nuevo proyecto Maven
  1. Introduce los datos para el proyecto Maven:

    • Group Id: org.ual.hmis

    • Artifact Id: nombreEquipo (sustituye nombreEquipo por el nombre de tu equipo)

    • Name: seleniumWebDriver

eclipse new maven project artifactid
Figura 4. Datos del proyecto Maven
  1. En la carpeta test crea un nuevo paquete en la carpeta test de nombre org.ual.hmis.nombreEquipo (sustituyendo nombreEquipo por el nombre de tu equipo). Ahí guarda los archivos .java exportados de Selenium IDE.

  2. Verás que aparecen muchos errores de compilación porque aun no hemos añadido las librerías necesarias de JUnit 4 y Selenium Webdriver. Para hacerlo, vamos a apoyarnos en Maven.

Solamente si inicialmente habías creado el proyecto de tipo Java en lugar de Maven, a continuación convierte el proyecto en un proyecto Maven: Sobre el nombre del proyecto, botón derecho, Configure > Convert to Maven project

eclipse convert to maven project
Figura 5. Convertir proyecto a Maven

Deja las opciones predeterminadas. Al mavenizar el proyecto, se crea el archivo pom.xml que va a contener toda la configuración necesaria para la construcción automatizada del proyecto, así como las dependencias (librerías) necesarias.

Para añadir las dependencias a JUnit 4 y a Selenium Webdriver, edita el archivo pom.xml seleccionando la pestaña para ver el código fuente xml.

eclipse edit pom
Figura 6. Editar el pom.xml

A continuación, añade las siguientes 4 dependencias en un nuevo bloque con la etiqueta <dependencies>, tras el bloque <build>…​</build>:

  ...
  </build>
  <dependencies>
  	<dependency> (1)
  		<groupId>junit</groupId>
  		<artifactId>junit</artifactId>
  		<version>4.13.2</version>
  	</dependency>
  	<dependency> (2)
  		<groupId>org.seleniumhq.selenium</groupId>
  		<artifactId>selenium-java</artifactId>
  		<version>4.21.0</version>
  	</dependency>
  	<dependency> (3)
  		<groupId>org.seleniumhq.selenium</groupId>
  		<artifactId>selenium-firefox-driver</artifactId>
  		<version>4.21.0</version>
  	</dependency>
  	<dependency> (4)
  		<groupId>org.seleniumhq.selenium</groupId>
  		<artifactId>selenium-chrome-driver</artifactId>
  		<version>4.21.0</version>
  	</dependency>
  </dependencies>
</project>
1 JUnit 4.
2 Selenium-Java, librería básica de Selenium Webdriver. Puedes ver la última versión disponible aquí: Selenium Java on Maven Repository
3 Selenium Firefox Driver, librería con el driver de Firefox para poder ejecutar los tests en este navegador. Puedes ver la última versión disponible aquí: Selenium Firefox Driver on Maven Repository
4 Selenium Chrome Driver, librería con el driver de Chrome para poder ejecutar los tests en este navegador. Puedes ver la última versión disponible aquí: Selenium Chrome Driver on Maven Repository

Una vez guardados los cambios, los errores de compilación en tu clase .java con los test exportados de Selenium deberían desaparecer.

En caso de dudas, utiliza como referencia el repositorio https://github.com/ualhmis/seleniumWebDriverJUnit que tiene ya configurado el pom.xml con las dependencias necesarias.

Los test de JUnit exportados por Selenium IDE están en formato JUnit 4. Para reutilizar al máximo y no tener que hacer demasiadas modificaciones de los test exportados desde Selenium IDE, la dependencia en el pom.xml está configurada a JUnit 4.

En el repositorio puedes encontrar la rama JUnit 5 donde se han adaptado los tests y las dependencias a JUnit 5.

2. Cross-browser testing en Eclipse

Cross-browser testing o prueba de navegadores cruzados es el proceso de probar aplicaciones y sitios web en los navegadores web más habituales que los usuarios utilizan en la actualidad. En esta sección, vamos a ejecutar los tests de Selenium en dos navegadores: Firefox y Chrome.

  1. Crea una carpeta drivers en el proyecto en Eclipse. Y añade la carpeta al .gitginore para que el contenido no se guarde en el repositorio.

.gitignore
**/bin
**/target
**/drivers

Es importante que los drivers específicos de los navegadores no se guarden en el repositorio, ya que son archivos ejecutables (dependencias) específicos de la plataforma, que no deben versionarse. Cuando los necesites, tendrás que descargarlos de la fuente adecuada.

  1. En esa carpeta descarga los drivers de los navegadores Firefox y Chrome. Para ello:

    1. Descarga y descomprime Firefox driver (Gecko Driver) de https://github.com/mozilla/geckodriver/releases eligiendo la versión adecuada para tu sistema.

    2. Descarga y descomprime Chrome driver de https://googlechromelabs.github.io/chrome-for-testing/ eligiendo la versión adecuada para tu sistema y la versión de Chrome que tengas instalada. Por ejemplo, para Windows descarga el archivo chromedriver-win64.zip y descomprímelo en la carpeta drivers del proyecto en Eclipse.

chrome driver 125
Figura 7. Descarga chrome driver

Con estos dos drivers es suficiente como prueba de concepto, pero puedes ver como descargar los drivers de otros navegadores aquí: Supported Browsers; y aquí Selenium Webdriver Ecosystem.

  1. De la sección anterior, deberás tener un paquete en la carpeta test de nombre org.ual.hmis.nombreEquipo (sustituyendo nombreEquipo por el nombre de tu equipo), y ahí estarán archivos .java exportados de Selenium IDE. Si has forkeado el proyecto de referencia, elimina los paquetes que contienen tests de ejemplo.

  2. A continuación se indican unas mínimas modificaciones que hay que realizar a cada archivo fuente .java exportado de Selenium IDE:

    1. Añade el paquete a cada archivo .java

    2. En el método setUp(), añade justo al principio las sentencias para configurar la ruta a cada driver:

  @Before
  public void setUp() throws Exception {
    System.setProperty("webdriver.gecko.driver", "drivers/geckodriver.exe"); (1)
    System.setProperty("webdriver.chrome.driver", "drivers/chromedriver.exe"); (2)
    ...
  }
1 Son rutas relativas en el proyecto, dentro de drivers hemos descargado los drivers. Usa la ruta adecuada en tu caso.
2 Idem
  1. Ejecuta los tests como JUnit Test. Verás que automáticamente se abre Firefox y ejecuta los test. Si los tests fallan, revisa el código de los mismos y aplica las recomendaciones de la sección "Corrigiendo errores habituales" de este documento.

  2. A continuación vamos a probar en otro navegador, haciendo así lo que se denomina cross-browser testing. En los archivos .java cambia el driver a Chrome:

  @Before
  public void setUp() throws Exception {
    ...
    // driver = new FirefoxDriver();
    driver = new ChromeDriver();
    ...
  }
  1. Vuelve a ejecutar y verás que se abre Chrome y ejecuta el mismo test.

  2. Por último, a continuación se muestra el código con una propuesta de diseño mejorado para el método setup(), en el que se define una variable int browser para elegir el navegador, y un booleano headless que permite establecer el modo headless (que se describe más adelante en este documento):

  @Before
  public void setUp() {
    // Browser selector
    int browser= 0; // 0: firefox, 1: chrome,...
    Boolean headless = false;

    switch (browser) {
    case 0:  // firefox
    	// Firefox
    	// Descargar geckodriver de https://github.com/mozilla/geckodriver/releases
    	// Descomprimir el archivo geckodriver.exe en la carpeta drivers

    	System.setProperty("webdriver.gecko.driver",  "drivers/geckodriver.exe");
    	FirefoxOptions firefoxOptions = new FirefoxOptions();
    	if (headless) firefoxOptions.addArguments("--headless"); // .setHeadless(headless);
    	driver = new FirefoxDriver(firefoxOptions);

    	break;
    case 1: // chrome
    	// Chrome
    	// Descargar Chromedriver de https://chromedriver.chromium.org/downloads
    	// Descomprimir el archivo chromedriver.exe en la carpeta drivers

    	System.setProperty("webdriver.chrome.driver", "drivers/chromedriver.exe");
    	ChromeOptions chromeOptions = new ChromeOptions();
    	if (headless) chromeOptionschromeOptions.addArguments("--headless=new"); // .setHeadless(headless);
    	chromeOptions.addArguments("window-size=1920,1080");
    	driver = new ChromeDriver(chromeOptions);

    	break;

    default:
    	fail("Please select a browser");
    	break;
    }
    js = (JavascriptExecutor) driver;
    vars = new HashMap<String, Object>();
  }

3. Corrigiendo errores habituales

Si un test falla al ejecutarlo con Webdriver, revisa el código y los pasos incluidos en el mismo. Puede haber pasos que sobren, ya que muchas veces Selenium IDE recoge acciones sobre el navegador que no son realmente necesarias, o tal vez haya pasos que que al exportarlos a JUnit tengas que adaptarlos a Java. A continuación se indican soluciones a los motivos de error más habituales:

3.1. Selectores

Uno de los principales motivos de fallo se debe al selector Selenium IDE que ha tomado automáticamente. El selector identifica el elemento dentro de la página web sobre el que se ha interactuado, y para ello utiliza bien la referencia por ID, NAME, CSS o XPATH. Ve a Selenium IDE y cambia el selector, en la propiedad target; es recomendable utilizar la opción que identifica el elemento id pero si no es posible porque el elemento de la página web sobre el que se desea interactuar no tiene id, utiliza el identificador por xpath y el texto que queremos seleccionar. Por ejemplo, en un comando click:

selenium ide change selector xpath
Figura 8. Cambiar el selector de un elemento de la página

3.2. Esperas

En numerosas ocasiones cuando se le indica al navegador que navegue a una página y a continuación se intenta encontrar un elemento en esa página, se obtiene un error indicando que no existe tal elemento. Esto es debido al tiempo necesario para que se cargue el contenido de la página, que ha sido superior al tiempo esperado por Selenium para ejecutar la acción. Es por ello que se hace necesario añadir un tiempo de espera en medio de determinadas acciones para permitir que se cargue el contenido del formulario, la página, etc. Por ejemplo, antes del primer sendKeys que escribe un texto en un campo de formulario, y también antes y después de click() en un botón de formulario.

Selenium tiene varias estrategias de espera, principalmente esperas explícitas y esperas implícitas. Podemos añadir esperas explícitas de dos formas para que el código detenga la ejecución del programa, o congelar el hilo, hasta que la condición que le pases se resuelva:

  • añadiendo un tiempo fijo (no recomendado)

Añade una espera de tiempo fijo de 1 segundo (1000 mls)
	    try {
	        Thread.sleep(1000);
	      } catch (InterruptedException e) {
	        e.printStackTrace();
	      }
  • Alternativa a meter segundos de espera con Time.sleep(). En Selenium IDE existe el comando waitForElementVisible que permite esperar hasta que un elemento esté visible. Aunque al grabar el test con Selenium IDE no es necesario añadir este comando, porque ya lo tiene implícito, cuando exportamos a WebDriver sí necesitamos meter las esperas. Por tanto es recomendable este comando en todos los pasos del test que veas que tarda en cargar la página.

selenium ide waitForElementVisible
Figura 9. Comando waitForElementVisible

En JUnit se convierte en:

selenium webdriver waitForElementVisible
Figura 10. Código JUnit para waitForElementVisible
// 9 | waitForElementVisible | xpath="//h2[contains(.,\'Ingeniería y Arquitectura\')]" | 30000
{
  WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
  wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//h2[contains(.,\'Ingeniería y Arquitectura\')]")));
}

En caso de que siga sin funcionar, sustituye el método visibilityOfElementLocated por otro de la misma clase ExpectedConditions`, por ejemplo elementToBeClickable (RECOMENDADO!):

// 9 | waitForElementVisible | xpath="//h2[contains(.,\'Ingeniería y Arquitectura\')]" | 30000
{
  WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
  wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//h2[contains(.,\'Ingeniería y Arquitectura\')]")));
}

Otro método de espera es la espera implícita, que aparece en la documentación de Selenium y en los ejemplos, sin embargo no dan el resultado esperado. Por ejemplo el método implicitlyWait:

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

Se supone que establece la cantidad de tiempo que el driver debería esperar cuando busca un elemento si este no está presente inmediatamente. Sin embargo, no funciona siempre como se espera si no se están usando las últimas versiones de las dependencias y de los navegadores, y los errores no se solucionan.

Así que la forma más segura es añadir manualmente esperas explícitas en cada paso que requiera tiempo de carga de los contenidos. Puesto que añadir un sleep() está desaconsejado, la mejor opción entonces es añadir comandos waitForElementVisible y su equivalente en Webdriver wait.until(ExpectedConditions.elementToBeClickable…​).

3.3. Otras comprobaciones

Si un test se ejecuta correctamente en Firefox pero falla en Chrome realiza las siguientes comprobaciones:

  • comprueba el tamaño de la ventana, agrándala por si es el problema:

    driver.manage().window().setSize(new Dimension(1080, 824));
  • modifica los selectores, en lugar de cssSelector utiliza xpath

  • Añade un tiempo de espera a que se cargue el formulario, antes del primer sendKeys, y también después de click() en un botón de formulario.

  • Revisa el idioma predeterminado en la configuración de cada navegador. Puede que uno navegador lo tengas configurado en español y otro en inglés, y por tanto los textos se visualicen en diferentes idiomas.

4. Configurar un driver headless

El modo headless sirve para ejecutar los tests sin que se visualice la ventana del navegador. Esto hace que los tests se ejecuten más rápido y más eficientemente, y es especialmente adecuado en un entorno de Integración Continua como Jenkins.

4.1. Firefox en modo headless

En local, para ejecutar Firefox en modo headless añade las siguientes sentencias:

  @Before
  public void setUp() throws Exception {
    ...
    FirefoxOptions firefoxOptions = new FirefoxOptions(); (1)
    firefoxOptions.setHeadless(true); (2)
    driver = new FirefoxDriver(firefoxOptions);
    ...
  }
1 Define un nuevo objeto de opciones
2 Establece la opción headless a true

Además deberás añadir los imports necesarios (Eclipse te avisa de ello):

import org.openqa.selenium.firefox.FirefoxOptions;

Prueba a ejecutar los tests y verás que se ejecutan sin visualizar la ventana de Firefox.

Lanza los tests tanto con Eclipse como con Maven. Aseguraté de que se ejecutan correctamente con maven test.

4.2. ChromeDriver en modo headless

ChromeDriver funciona de manera similar a Geckodriver de Firefox, e implementa la especificación W3C WebDriver.

En local, para ejecutar Chrome en modo headless:

  @Before
  public void setUp() throws Exception {
    ...
    ChromeOptions chromeOptions = new ChromeOptions(); (1)
    chromeOptions.setHeadless(true); (2)
    driver = new ChromeDriver(chromeOptions);
    ...
  }
1 Define un nuevo objeto de opciones
2 Establece la opción headless a true

Además deberás añadir los imports:

import org.openqa.selenium.chrome.ChromeOptions;

Durante la ejecución no se abrirá la ventana de Chrome y los tests se ejecutarán correctamente. Lanza los test tanto con Eclipse como con Maven.

4.3. HtmlUnit Driver: modo headless nativo

HtmlUnit Driver es un driver headless nativo para Selenium Webdriver. Se trata de una implementación en Java de Webdriver.

Para usarlo, hay que añadir la dependencia a HtmlUnit Driver en el pom:

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>htmlunit-driver</artifactId>
    <version>3.61.0</version>
</dependency>

La versión 3.x es compatible con Selenium 4.

En local, comenta los otros drivers y cambia el driver a HtmlUnitDriver();

import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import com.gargoylesoftware.htmlunit.BrowserVersion;

  @Before
  public void setUp() throws Exception {
    ...
    // simple case - javascript support enabled
    WebDriver driver = new HtmlUnitDriver(BrowserVersion.FIREFOX, true);
    ...
  }

HtmlUnit Driver da muchos problemas, sobre todo con JavaScript. Es la versión reducida de un navegador, por lo que no soporta gran parte de la funcionalidad del mismo, y la mayoría de tests que funcionan para FirefoxDriver y ChromeDriver fallan con HtmlUnitDriver. Si te fallan los test HtmlUnitDriver no te preocupes. El modo headless de FirefoxDriver y ChromeDriver nos ayudará a nuestro objetivo.

5. FAQ y resolución de problemas (thoubleshouting)

  • Problema al actualizar un campo de texto que ya contiene un valor. Cuando un test de Selenium trata de actualizar el valor de un campo de texto que ya contiene un valor, por ejemplo al modificar el email del perfil de usuario, el test grabado con Selenium IDE simplemente captura los eventos que guardan el nuevo valor mediante un comando type y el nuevo valor se guarda en la propiedad Value.

login app update profile ide commands
Figura 11. Comandos Selenium IDE para actualizar el email
1 Nuevo valor de email

La siguiente imagen muestra la vista de la app web, antes de escribir el nuevo valor, muestra el valor antiguo.

login app update profile
Figura 12. Vista de la app web para actualizar el email (se muestra el valor antiguo)

El problema se produce al exportar a JUnit el comando type, se traduce en una llamada al método sendkeys("nuevoValor"). Por ejemplo:

  driver.findElement(By.Id("email-address")).sendKeys("ualkk000@ual.es");

Y el método sendkeys("nuevoValor") no reemplaza el valor existente, sino que concatena el valor existente con el nuevo, algo asi: ual-744547@ual.esualkk000@ual.es Para solucionar este problema simplemente hay que llamar al método clear(), que limpia el contenido del campo de texto, y tras ello escribir el nuevo valor con sendKeys(…​)

  driver.findElement(By.Id("email-address")).clear(); (1)
  driver.findElement(By.Id("email-address")).sendKeys("ualkk000@ual.es");
1 Añadir manualmente la llamada a clear() para limpiar el contenido del campo de texto.

La llamada a clear() también será útil si se quiere probar el caso de dejar en blanco el campo email.

En el caso de que clear() no funcione, podemos probar la siguiente combinación de teclas, bien en una llamada o bien en dos:

driver.findElement(By.Id("email-address")).sendKeys(Keys.chord(Keys.CONTROL,"a", Keys.DELETE));

driver.findElement(By.Id("email-address")).sendKeys(Keys.chord(Keys.CONTROL,"a"));
driver.findElement(By.Id("email-address")).sendKeys(Keys.chord(Keys.DELETE));
  • Navegador headless queda ejecutándose en background. Cuando falla la ejecución de un test de Selenium WebDriver en modo headless, el navegador puede quedar ejecutándose en background, pudiendo consumir recursos del sistema innecesariamente.

google chrome procesos sin morir
Figura 13. Decenas de procesos sin morir del navegador en modo headless

Es necesario revisar los procesos tanto en nuestra máquina local como en Jenkins y matarlos para evitar que queden ejecutándose consumiendo recursos.

6. Más info