Selenium

Tutorial Selenium: Patrón Page Object Model (POM)

Aprende a implementar el patrón Page Object Model (POM) en Selenium con JUnit para aprovechar al máximo las capacidades de Selenium y reutilizar el código de pruebas de forma escalable.

Aprende Selenium GRATIS y certifícate

Concepto y ventajas de POM

El Patrón Page Object Model (POM) es una forma de estructurar el código en las pruebas automatizadas de aplicaciones web. Este patrón propone crear una clase para cada página o componente significativo de la aplicación, donde se encapsulan tanto los elementos de la interfaz de usuario como las acciones que se pueden realizar sobre ellos. Esto permite separar la lógica de las pruebas de la implementación de la interfaz, mejorando la mantenibilidad y la legibilidad del código.

Una de las principales ventajas de POM es que facilita el manejo de cambios en la interfaz de usuario. Si, por ejemplo, cambia el identificador de un botón o se modifica la estructura de una página, solo es necesario actualizar la clase correspondiente. De este modo, las pruebas que utilizan esa clase no necesitan ser modificadas, lo que reduce significativamente el esfuerzo de mantenimiento.

Además, POM promueve la reutilización de código. Al tener métodos que representan acciones comunes en una página, como hacer clic en un botón o rellenar un formulario, estos pueden ser utilizados en múltiples pruebas sin necesidad de duplicar código. Esto no solo ahorra tiempo, sino que también evita errores al mantener la lógica en un único lugar.

Otra ventaja es que las pruebas escritas utilizando POM son más claras y fáciles de entender. Al abstraer las interacciones con la interfaz de usuario, las pruebas se centran en la lógica de negocio y en los escenarios de prueba, lo que facilita su comprensión por parte de otros desarrolladores o miembros del equipo.

Por ejemplo, sin utilizar POM, una prueba podría verse así:

@Test
void iniciarSesion() {
    driver.findElement(By.id("usuario")).sendKeys("admin");
    driver.findElement(By.id("contrasena")).sendKeys("1234");
    driver.findElement(By.id("boton-login")).click();
    // Verificaciones posteriores
}

Con POM, primero se define una clase que representa la página de inicio de sesión:

public class PaginaInicioSesion {
    private WebDriver driver;
    private By campoUsuario = By.id("usuario");
    private By campoContrasena = By.id("contrasena");
    private By botonLogin = By.id("boton-login");

    public PaginaInicioSesion(WebDriver driver) {
        this.driver = driver;
    }

    public void iniciarSesion(String usuario, String contrasena) {
        driver.findElement(campoUsuario).sendKeys(usuario);
        driver.findElement(campoContrasena).sendKeys(contrasena);
        driver.findElement(botonLogin).click();
    }
}

Y la prueba quedaría de la siguiente manera:

@Test
void iniciarSesion() {
    PaginaInicioSesion paginaInicio = new PaginaInicioSesion(driver);
    paginaInicio.iniciarSesion("admin", "1234");
    // Verificaciones posteriores
}

Como se puede observar, la prueba es más concisa y enfocada en el objetivo que se quiere verificar. Además, si en el futuro cambia el identificador del botón de inicio de sesión, solo se debe actualizar en la clase PaginaInicioSesion, sin necesidad de modificar todas las pruebas que lo utilizan.

El uso de POM también facilita la implementación de buenas prácticas en el diseño de pruebas automatizadas, como el principio de responsabilidad única y la adherencia al patrón de diseño orientado a objetos. Esto resulta en un código más estructurado y fácil de mantener a largo plazo.

En resumen, el patrón Page Object Model aporta las siguientes ventajas en el desarrollo de pruebas con Selenium:

  • Mantenibilidad: Centraliza los cambios en la interfaz de usuario en un solo lugar.
  • Reutilización: Permite utilizar las mismas clases de página en múltiples pruebas.
  • Claridad: Mejora la legibilidad de las pruebas al separar la lógica de negocio de los detalles de la interfaz.
  • Reducción de duplicidad: Evita la repetición de código al encapsular acciones comunes.
  • Facilidad de actualización: Simplifica la adaptación a cambios en la aplicación bajo prueba.

Implementar POM es una práctica recomendada que optimiza el proceso de desarrollo de pruebas automatizadas y contribuye a la calidad del software entregado.

Implementación básica de POM

Para implementar el Page Object Model de manera eficaz en Selenium, es fundamental crear una clase por cada página o componente significativo de la aplicación web. Cada una de estas clases encapsula los elementos web y las acciones que se pueden realizar sobre ellos, lo que mejora la mantenibilidad y la reutilización del código.

Comencemos creando una clase que represente la página de inicio de sesión. Esta clase contendrá los localizadores de los elementos y los métodos necesarios para interactuar con ellos.

public class PaginaInicioSesion {
    private WebDriver driver;

    // Localizadores de los elementos
    private By campoUsuario = By.id("usuario");
    private By campoContrasena = By.id("contrasena");
    private By botonIniciarSesion = By.id("inicioSesion");

    // Constructor de la clase
    public PaginaInicioSesion(WebDriver driver) {
        this.driver = driver;
    }

    // Métodos para interactuar con los elementos
    public PaginaInicioSesion ingresarUsuario(String usuario) {
        driver.findElement(campoUsuario).sendKeys(usuario);
        return this;
    }

    public PaginaInicioSesion ingresarContrasena(String contrasena) {
        driver.findElement(campoContrasena).sendKeys(contrasena);
        return this;
    }

    public void presionarBotonIniciarSesion() {
        driver.findElement(botonIniciarSesion).click();
    }

    // Método que encapsula el inicio de sesión completo
    public void iniciarSesionComo(String usuario, String contrasena) {
        ingresarUsuario(usuario)
            .ingresarContrasena(contrasena)
            .presionarBotonIniciarSesion();
    }
}

En este ejemplo, se utilizan técnicas de encadenamiento de métodos para facilitar la lectura y permitir una sintaxis más fluida. Los métodos devuelven la instancia actual (this), lo que permite concatenarlos en una única línea.

A continuación, se muestra cómo utilizar esta clase en un caso de prueba con JUnit 5:

import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class PruebasDeInicioSesion {

    private WebDriver driver;
    private PaginaInicioSesion paginaInicioSesion;

    @BeforeEach
    void configurar() {
        driver = new ChromeDriver();
        driver.get("https://www.ejemplo.com/inicio-sesion");
        paginaInicioSesion = new PaginaInicioSesion(driver);
    }

    @AfterEach
    void finalizar() {
        driver.quit();
    }

    @Test
    void iniciarSesionConCredencialesValidas() {
        paginaInicioSesion.iniciarSesionComo("usuarioValido", "contrasenaValida");
        // Verificar que el inicio de sesión fue exitoso
    }
}

Este enfoque mejora la legibilidad de los tests al abstraer las interacciones con la página. La prueba se centra en el comportamiento y no en los detalles de implementación, lo que facilita su mantenimiento.

Es recomendable utilizar esperas explícitas para manejar la sincronización con elementos que pueden tardar en cargarse. Podemos integrar las esperas en la clase de la página:

import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;

public class PaginaInicioSesion {
    private WebDriver driver;
    private WebDriverWait espera;

    // Localizadores de los elementos
    private By campoUsuario = By.id("usuario");
    private By campoContrasena = By.id("contrasena");
    private By botonIniciarSesion = By.id("inicioSesion");

    public PaginaInicioSesion(WebDriver driver) {
        this.driver = driver;
        this.espera = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public PaginaInicioSesion ingresarUsuario(String usuario) {
        espera.until(ExpectedConditions.visibilityOfElementLocated(campoUsuario)).sendKeys(usuario);
        return this;
    }

    public PaginaInicioSesion ingresarContrasena(String contrasena) {
        espera.until(ExpectedConditions.visibilityOfElementLocated(campoContrasena)).sendKeys(contrasena);
        return this;
    }

    public void presionarBotonIniciarSesion() {
        espera.until(ExpectedConditions.elementToBeClickable(botonIniciarSesion)).click();
    }
}

Al incorporar WebDriverWait, aseguramos que los elementos están disponibles antes de interactuar con ellos, lo que aumenta la robustez de las pruebas.

Para validar que el inicio de sesión fue exitoso, podemos crear otra clase que represente la página de bienvenida o dashboard:

public class PaginaDashboard {
    private WebDriver driver;

    // Localizador de un elemento representativo
    private By tituloPagina = By.tagName("h1");

    public PaginaDashboard(WebDriver driver) {
        this.driver = driver;
    }

    public String obtenerTituloPagina() {
        return driver.findElement(tituloPagina).getText();
    }
}

En el test, después de iniciar sesión, podemos verificar que el usuario ha llegado al dashboard:

@Test
void iniciarSesionConCredencialesValidas() {
    paginaInicioSesion.iniciarSesionComo("usuarioValido", "contrasenaValida");
    PaginaDashboard paginaDashboard = new PaginaDashboard(driver);
    Assertions.assertEquals("Bienvenido", paginaDashboard.obtenerTituloPagina(), "El usuario no fue redirigido al dashboard correctamente");
}

Al separar las páginas en diferentes clases, aplicamos el principio de responsabilidad única, lo que facilita la gestión y escalabilidad del proyecto.

También es importante manejar escenarios negativos, como intentos de inicio de sesión con credenciales inválidas. Podemos agregar métodos para verificar mensajes de error:

public class PaginaInicioSesion {
    // ... código anterior ...

    private By mensajeError = By.id("mensajeError");

    public String obtenerMensajeDeError() {
        return driver.findElement(mensajeError).getText();
    }
}

Y en el test:

@Test
void iniciarSesionConCredencialesInvalidas() {
    paginaInicioSesion.iniciarSesionComo("usuarioInvalido", "contrasenaIncorrecta");
    String mensajeDeError = paginaInicioSesion.obtenerMensajeDeError();
    Assertions.assertTrue(mensajeDeError.contains("Credenciales incorrectas"), "El mensaje de error no coincide");
}

Este enfoque permite que las pruebas sean más claras y enfocadas en los resultados esperados, facilitando la detección de errores en la aplicación.

La utilización de programación funcional en Java puede optimizar aún más nuestro código. Por ejemplo, podemos utilizar expresiones lambda para inicializar el WebDriver de manera más concisa:

import java.util.function.Supplier;

public class BaseTest {
    protected WebDriver driver;

    @BeforeEach
    void configurar() {
        Supplier<WebDriver> crearDriver = ChromeDriver::new;
        driver = crearDriver.get();
    }

    @AfterEach
    void finalizar() {
        driver.quit();
    }
}

Al extender de BaseTest, nuestras clases de prueba heredan la configuración del WebDriver, lo que promueve la reutilización y evita duplicación de código.

Además, es buena práctica centralizar los localizadores y evitar valores hardcoded. Si los selectores cambian, solo necesitamos actualizar la clase correspondiente, manteniendo las pruebas intactas. Esto refuerza la flexibilidad del diseño POM.

Finalmente, mantener una nomenclatura coherente y descriptiva en los nombres de clases, métodos y variables mejora la legibilidad y facilita la colaboración entre desarrolladores. Al seguir estas pautas, la implementación básica de POM se convierte en una herramienta poderosa para escribir pruebas automatizadas eficaces y sostenibles.

Uso de PageFactory

El PageFactory es una herramienta que facilita la implementación del Page Object Model (POM) en Selenium, permitiendo una inicialización más sencilla y legible de los elementos web. Al utilizar anotaciones y mecanismos de reflexión, PageFactory automatiza el proceso de localización de elementos, mejorando la eficiencia y la organización del código.

Para aprovechar las ventajas de PageFactory, es necesario seguir estos pasos:

  1. Declarar los elementos web en la clase de la página utilizando anotaciones como @FindBy.
  2. Inicializar los elementos con el método initElements proporcionado por PageFactory.

A continuación, se muestra cómo implementar PageFactory en la clase PaginaInicioSesion:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.PageFactoryFinder;

public class PaginaInicioSesion {

    private WebDriver driver;

    // Inicialización de elementos con @FindBy
    @FindBy(id = "usuario")
    private WebElement campoUsuario;

    @FindBy(id = "contrasena")
    private WebElement campoContrasena;

    @FindBy(id = "inicioSesion")
    private WebElement botonIniciarSesion;

    // Constructor que inicializa los elementos
    public PaginaInicioSesion(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    // Métodos de interacción
    public PaginaInicioSesion ingresarUsuario(String usuario) {
        campoUsuario.sendKeys(usuario);
        return this;
    }

    public PaginaInicioSesion ingresarContrasena(String contrasena) {
        campoContrasena.sendKeys(contrasena);
        return this;
    }

    public void presionarBotonIniciarSesion() {
        botonIniciarSesion.click();
    }

    public void iniciarSesionComo(String usuario, String contrasena) {
        ingresarUsuario(usuario)
            .ingresarContrasena(contrasena)
            .presionarBotonIniciarSesion();
    }
}

En este ejemplo, los elementos se declaran como variables de tipo WebElement y se anotan con @FindBy, especificando el método de localización adecuado. El constructor de la clase invoca PageFactory.initElements, que se encarga de inicializar los elementos declarados.

El uso de anotaciones proporciona una sintaxis más limpia y elimina la necesidad de escribir código repetitivo para localizar elementos. Además, si en el futuro cambia la forma de identificar un elemento, solo es necesario actualizar la anotación correspondiente.

Para utilizar esta clase en una prueba con JUnit 5, se puede escribir lo siguiente:

import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class PruebasDeInicioSesion {

    private WebDriver driver;
    private PaginaInicioSesion paginaInicioSesion;

    @BeforeEach
    void configurar() {
        driver = new ChromeDriver();
        driver.get("https://www.ejemplo.com/inicio-sesion");
        paginaInicioSesion = new PaginaInicioSesion(driver);
    }

    @AfterEach
    void finalizar() {
        driver.quit();
    }

    @Test
    void iniciarSesionConCredencialesValidas() {
        paginaInicioSesion.iniciarSesionComo("usuarioValido", "contrasenaValida");
        // Verificaciones posteriores
    }
}

Al emplear PageFactory, la prueba se vuelve más concisa y se centra en el flujo lógico, sin preocuparse por los detalles de localización de los elementos.

Es posible utilizar otros tipos de localizadores con las anotaciones de PageFactory, como @FindBy(name = "nombre"), @FindBy(xpath = "//div[@id='ejemplo']") o combinaciones más complejas:

@FindBy(css = "input[type='email']")
private WebElement campoEmail;

@FindBy(linkText = "Olvidé mi contraseña")
private WebElement enlaceOlvideContrasena;

Para elementos que requieren una localización más sofisticada, se pueden utilizar múltiples criterios con la anotación @FindBys:

import org.openqa.selenium.support.FindBys;

@FindBys({
    @FindBy(className = "contenedor"),
    @FindBy(tagName = "input")
})
private WebElement primerInputEnContenedor;

Esta capacidad de combinar criterios de búsqueda incrementa la flexibilidad en la definición de los elementos de una página.

Además de WebElement, es posible declarar listas de elementos utilizando List<WebElement> para manejar colecciones:

@FindBy(tagName = "option")
private List<WebElement> opcionesSeleccion;

Esto es útil cuando se necesita interactuar con múltiples elementos similares, como los ítems de una lista o los resultados de una búsqueda.

Otra característica relevante es la posibilidad de utilizar PageFactory con componentes dinámicos. Si una página contiene elementos que cambian de forma dinámica, se puede utilizar la anotación @CacheLookup para indicar que un elemento debe ser almacenado en caché después de ser localizado:

import org.openqa.selenium.support.CacheLookup;

@CacheLookup
@FindBy(id = "elementoEstatico")
private WebElement elementoEstatico;

Sin embargo, es importante usar @CacheLookup con precaución, ya que si el elemento se actualiza o desaparece del DOM, se podría obtener una excepción StaleElementReferenceException.

Para mejorar la sincronización al interactuar con elementos, es recomendable integrar esperas explícitas directamente en los métodos de la clase de la página:

import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

public class PaginaInicioSesion {

    private WebDriver driver;
    private WebDriverWait espera;

    // ... elementos con @FindBy ...

    public PaginaInicioSesion(WebDriver driver) {
        this.driver = driver;
        this.espera = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(driver, this);
    }

    // Métodos de interacción con esperas
    public PaginaInicioSesion ingresarUsuario(String usuario) {
        espera.until(ExpectedConditions.visibilityOf(campoUsuario)).sendKeys(usuario);
        return this;
    }

    public PaginaInicioSesion ingresarContrasena(String contrasena) {
        espera.until(ExpectedConditions.visibilityOf(campoContrasena)).sendKeys(contrasena);
        return this;
    }

    public void presionarBotonIniciarSesion() {
        espera.until(ExpectedConditions.elementToBeClickable(botonIniciarSesion)).click();
    }

    // ... resto de la clase ...
}

Al incorporar esperas en los métodos, se asegura que los elementos estén en el estado adecuado antes de interactuar con ellos, aumentando la robustez de las pruebas.

También es posible crear clases base que contengan elementos y métodos comunes a varias páginas, promoviendo la herencia y la reutilización. Por ejemplo:

public abstract class PaginaBase {

    protected WebDriver driver;
    protected WebDriverWait espera;

    @FindBy(id = "barraNavegacion")
    protected WebElement barraNavegacion;

    @FindBy(id = "piePagina")
    protected WebElement piePagina;

    public PaginaBase(WebDriver driver) {
        this.driver = driver;
        this.espera = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(driver, this);
    }

    // Métodos comunes
    public void irASeccion(String seccion) {
        // Implementación para navegar a una sección específica
    }
}

Las páginas específicas pueden extender de PaginaBase y heredar sus elementos y métodos:

public class PaginaDashboard extends PaginaBase {

    @FindBy(tagName = "h1")
    private WebElement tituloPagina;

    public PaginaDashboard(WebDriver driver) {
        super(driver);
    }

    public String obtenerTituloPagina() {
        return tituloPagina.getText();
    }
}

Este enfoque permite estructurar las clases de página de manera más modular y mantener un código más limpio.

Por otra parte, es posible utilizar patrones de diseño avanzados como la inyección de dependencias para mejorar la gestión de los objetos WebDriver y WebDriverWait. Mediante frameworks como Guice o Spring, se puede inyectar el WebDriver en las clases de página, facilitando la configuración y el control de los recursos.

En cuanto a las pruebas, es posible parametrizar los datos de entrada utilizando Data Providers o fuentes externas como archivos CSV o bases de datos. Esto permite ejecutar la misma prueba con diferentes conjuntos de datos, mejorando la cobertura y la eficiencia del proceso de testing.

Un ejemplo de prueba parametrizada con JUnit 5 es el siguiente:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class PruebasDeInicioSesion {

    // ... configuración ...

    @ParameterizedTest
    @CsvSource({
        "usuarioValido, contrasenaValida, true",
        "usuarioInvalido, contrasenaValida, false",
        "usuarioValido, contrasenaInvalida, false"
    })
    void iniciarSesionConDiferentesCredenciales(String usuario, String contrasena, boolean exitoEsperado) {
        paginaInicioSesion.iniciarSesionComo(usuario, contrasena);
        boolean inicioExitoso = // lógica para verificar el inicio de sesión
        Assertions.assertEquals(exitoEsperado, inicioExitoso);
    }
}

Al combinar PageFactory con pruebas parametrizadas, se logra un conjunto de pruebas más completo y escalable.

Finalmente, es importante mantener buenas prácticas en la organización del proyecto, como separar las pruebas unitarias de las pruebas de integración, y utilizar patrones de nomenclatura consistentes. Documentar correctamente las clases y métodos también contribuye a un código más comprensible y fácil de mantener.

El uso de PageFactory en conjunto con POM y JUnit 5 permite construir un framework de pruebas automatizadas robusto y eficiente, alineado con las necesidades actuales del desarrollo de software.

Implementación avanzada de POM

En la implementación avanzada del Page Object Model (POM), se busca optimizar y robustecer las pruebas automatizadas mediante la incorporación de buenas prácticas y patrones de diseño. Esto permite manejar aplicaciones web complejas y mejorar la escalabilidad y mantenibilidad del código de pruebas.

Una técnica clave es la herencia en las clases de página. Al crear una clase base abstracta que encapsule elementos y métodos comunes a todas las páginas, se evita la duplicación de código y se promueve la reutilización. Por ejemplo:

public abstract class PaginaBase {

    protected WebDriver driver;

    // Constructor común para todas las páginas
    public PaginaBase(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    // Método para obtener el título de la página
    public String obtenerTitulo() {
        return driver.getTitle();
    }
}

Las páginas específicas extienden de PaginaBase, heredando sus propiedades y métodos:

public class PaginaInicio extends PaginaBase {

    @FindBy(id = "campoBusqueda")
    private WebElement campoBusqueda;

    public PaginaInicio(WebDriver driver) {
        super(driver);
    }

    public void buscarProducto(String producto) {
        campoBusqueda.sendKeys(producto);
        campoBusqueda.submit();
    }
}

Otra práctica avanzada es el uso del patrón Singleton para gestionar la instancia de WebDriver. Al implementar un Singleton, se garantiza que todas las clases de página utilicen el mismo driver, facilitando el manejo de la sesión del navegador:

public class DriverSingleton {

    private static WebDriver driver;

    private DriverSingleton() {
        // Constructor privado
    }

    public static WebDriver obtenerDriver() {
        if (driver == null) {
            driver = new ChromeDriver();
        }
        return driver;
    }

    public static void cerrarDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

Con este enfoque, las clases de página pueden obtener el driver de la siguiente manera:

public class PaginaInicio extends PaginaBase {

    public PaginaInicio() {
        super(DriverSingleton.obtenerDriver());
    }

    // Métodos de la página
}

Para manejar elementos dinámicos y mejorar el rendimiento, es útil emplear carga perezosa (lazy loading) de elementos. Esto se logra utilizando proxys que retrasan la inicialización de los elementos hasta que son realmente necesarios:

public class PaginaProducto extends PaginaBase {

    @FindBy(id = "detalleProducto")
    private WebElement detalleProducto;

    public PaginaProducto(WebDriver driver) {
        super(driver);
    }

    public String obtenerDetalleProducto() {
        return detalleProducto.getText();
    }
}

Al combinar PageFactory con el patrón Proxy, los elementos se inicializan al momento de su uso, optimizando el consumo de recursos y manejando eficientemente las páginas con muchos elementos.

La inyección de dependencias es otra técnica avanzada que mejora la modularidad y testabilidad del código. Utilizando frameworks como Guice o Spring o CDI de Jakarta EE, se pueden inyectar dependencias en las clases de página sin acoplamiento fuerte:

public class PaginaCarrito extends PaginaBase {

    @Inject
    public PaginaCarrito(WebDriver driver) {
        super(driver);
    }

    // Métodos de la página
}

Para gestionar la sincronización en aplicaciones con comportamientos asíncronos, se pueden crear métodos de espera personalizados. Por ejemplo, una espera que verifique la presencia de un elemento específico:

public void esperarElementoVisible(By localizador) {
    new WebDriverWait(driver, Duration.ofSeconds(15))
        .until(ExpectedConditions.visibilityOfElementLocated(localizador));
}

Este método puede ser incorporado en la clase base y reutilizado en las clases derivadas, mejorando la robustez de las pruebas ante variaciones en los tiempos de carga.

El manejo de configuraciones dinámicas es esencial en entornos avanzados. Es recomendable externalizar las configuraciones, como URLs de las páginas, credenciales y tiempos de espera, en archivos de propiedades o variables de entorno. De esta forma, se aumenta la flexibilidad y se facilita la ejecución en diferentes ambientes (desarrollo, pruebas, producción).

Por ejemplo, se puede crear una clase utilitaria para leer propiedades:

public class Configuracion {

    private static Properties propiedades;

    static {
        propiedades = new Properties();
        try (InputStream entrada = new FileInputStream("configuracion.properties")) {
            propiedades.load(entrada);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String obtenerPropiedad(String clave) {
        return propiedades.getProperty(clave);
    }
}

Y utilizarla en las pruebas:

String urlInicio = Configuracion.obtenerPropiedad("url.inicio");
driver.get(urlInicio);

En cuanto al diseño de pruebas, es recomendable implementar el patrón Data-Driven Testing, que permite separar los datos de prueba de la lógica. Al utilizar frameworks como JUnitParams o Parameterized Tests en JUnit 5, se pueden ejecutar pruebas con múltiples conjuntos de datos sin duplicar código:

@ParameterizedTest
@CsvFileSource(resources = "/datosPrueba.csv", numLinesToSkip = 1)
void pruebaInicioSesion(String usuario, String contrasena, String mensajeEsperado) {
    PaginaInicioSesion paginaInicio = new PaginaInicioSesion();
    paginaInicio.iniciarSesionComo(usuario, contrasena);
    String mensajeActual = paginaInicio.obtenerMensaje();
    Assertions.assertEquals(mensajeEsperado, mensajeActual);
}

Para manejar elementos reutilizables o componentes comunes en diferentes páginas, es útil crear clases que representen estos componentes. Por ejemplo, un menú de navegación que aparece en varias páginas:

public class MenuNavegacion {

    @FindBy(id = "menuPrincipal")
    private WebElement menuPrincipal;

    @FindBy(linkText = "Inicio")
    private WebElement linkInicio;

    public MenuNavegacion(WebDriver driver) {
        PageFactory.initElements(driver, this);
    }

    public void irAInicio() {
        linkInicio.click();
    }
}

Las páginas que incluyen este menú pueden tener una instancia de MenuNavegacion:

public class PaginaDashboard extends PaginaBase {

    private MenuNavegacion menuNavegacion;

    public PaginaDashboard(WebDriver driver) {
        super(driver);
        menuNavegacion = new MenuNavegacion(driver);
    }

    public void navegarAInicio() {
        menuNavegacion.irAInicio();
    }
}

Además, para mejorar la legibilidad y mantener el principio de fluidez, se pueden implementar métodos que permitan encadenar acciones, siguiendo el patrón Fluent Interface:

public class PaginaBusqueda extends PaginaBase {

    @FindBy(id = "campoBusqueda")
    private WebElement campoBusqueda;

    @FindBy(id = "botonBuscar")
    private WebElement botonBuscar;

    public PaginaBusqueda ingresarTermino(String termino) {
        campoBusqueda.clear();
        campoBusqueda.sendKeys(termino);
        return this;
    }

    public PaginaResultados presionarBuscar() {
        botonBuscar.click();
        return new PaginaResultados(driver);
    }
}

De esta manera, en las pruebas se puede escribir:

PaginaResultados resultados = new PaginaBusqueda(driver)
    .ingresarTermino("ordenador")
    .presionarBuscar();

Esta sintaxis es más intuitiva y refleja mejor el flujo de interacción con la aplicación.

Para gestionar pruebas en paralelo y mejorar el rendimiento, es crucial que las clases de página y las pruebas sean thread-safe. Evitar el uso de variables estáticas y garantizar que cada hilo tenga su propia instancia de WebDriver es fundamental. frameworks como TestNG o herramientas como Selenium Grid facilitan la ejecución concurrente de pruebas.

En aplicaciones modernas, es común interactuar con APIs REST además de la interfaz web. Integrar pruebas de API puede enriquecer el proceso de testing. Utilizando librerías como RestAssured, se pueden combinar pruebas de frontend y backend en un mismo framework:

public class ApiCliente {

    private static final String BASE_URL = Configuracion.obtenerPropiedad("api.base.url");

    public Response obtenerDatosUsuario(int idUsuario) {
        return given()
            .baseUri(BASE_URL)
            .pathParam("id", idUsuario)
        .when()
            .get("/usuarios/{id}");
    }
}

En las pruebas, es posible validar datos presentados en la interfaz web contra los datos obtenidos de la API, asegurando la consistencia de la aplicación.

Finalmente, implementar logs y generar reportes detallados es esencial en proyectos avanzados. Utilizar herramientas como Log4j para el registro de actividades y Allure para reportes interactivos permite un mejor seguimiento y análisis de las pruebas:

public class PaginaInicioSesion extends PaginaBase {

    private static final Logger logger = LogManager.getLogger(PaginaInicioSesion.class);

    public void iniciarSesionComo(String usuario, String contrasena) {
        logger.info("Iniciando sesión con el usuario: {}", usuario);
        ingresarUsuario(usuario)
            .ingresarContrasena(contrasena)
            .presionarBotonIniciarSesion();
    }
}

Integrar estos componentes en el framework de pruebas contribuye a una mejor trazabilidad y facilita la detección de errores.

En resumen, la implementación avanzada de POM implica no solo estructurar las clases de página, sino también incorporar patrones de diseño, técnicas de programación y herramientas que mejoren la calidad y eficiencia de las pruebas automatizadas. Esto se traduce en un código más sólido, flexible y preparado para adaptarse a los constantes cambios en las aplicaciones web modernas.

Aprende Selenium GRATIS online

Todas las lecciones de Selenium

Accede a todas las lecciones de Selenium y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Selenium y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Conocer qué son y para qué sirven los patrones de diseño
  • Entender el patrón Page Object Model
  • Implementar el patrón Page Object Model en Selenium
  • Uso de PageFactory