Introducción a PageFactory
PageFactory es una extensión del patrón Page Object Model que forma parte del conjunto de utilidades de soporte de Selenium WebDriver. Esta clase proporciona una metodología declarativa para inicializar elementos web mediante anotaciones, simplificando significativamente la creación y mantenimiento de Page Objects.
La principal ventaja de PageFactory radica en su capacidad para automatizar la inicialización de elementos web. En lugar de escribir manualmente cada driver.findElement() en el constructor de nuestras clases Page Object, PageFactory utiliza reflexión para detectar campos anotados y crear automáticamente los WebElement correspondientes.
Diferencias con Page Object tradicional
Mientras que en un Page Object tradicional definimos elementos de esta manera:
public class LoginPageTraditional {
private WebDriver driver;
private WebElement usernameInput;
private WebElement passwordInput;
private WebElement loginButton;
public LoginPageTraditional(WebDriver driver) {
this.driver = driver;
this.usernameInput = driver.findElement(By.id("username"));
this.passwordInput = driver.findElement(By.id("password"));
this.loginButton = driver.findElement(By.className("login-btn"));
}
}
Con PageFactory, el mismo resultado se consigue de forma más elegante:
public class LoginPage {
private WebDriver driver;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(className = "login-btn")
private WebElement loginButton;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
}
Inicialización con PageFactory.initElements()
El método PageFactory.initElements() es el núcleo de esta funcionalidad. Este método estático recibe dos parámetros: la instancia de WebDriver y el objeto que contiene los campos anotados (típicamente this cuando se llama desde el constructor).
public class ProductPage {
private WebDriver driver;
@FindBy(css = ".product-title")
private WebElement productTitle;
@FindBy(xpath = "//button[text()='Add to Cart']")
private WebElement addToCartButton;
public ProductPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public String getProductTitle() {
return productTitle.getText();
}
public void addToCart() {
addToCartButton.click();
}
}
Ventajas del enfoque declarativo
La aproximación declarativa de PageFactory ofrece múltiples beneficios. El código resulta más limpio y expresivo, ya que la intención de cada elemento queda clara desde su declaración. La separación entre la definición de elementos y la lógica de negocio mejora la legibilidad del código.
Además, PageFactory implementa un patrón proxy que retrasa la búsqueda de elementos hasta que realmente se necesitan. Esto significa que los elementos no se buscan en el DOM durante la inicialización, sino cuando se invoca algún método sobre ellos.
@Test
void testProductInteraction() {
var productPage = new ProductPage(driver);
// En este punto, los elementos aún no se han buscado en el DOM
var title = productPage.getProductTitle();
// Ahora sí se ejecuta la búsqueda del elemento productTitle
productPage.addToCart();
// Y aquí se busca addToCartButton
}
Integración con JUnit 5
En el contexto de pruebas automatizadas con JUnit 5, PageFactory se integra naturalmente en nuestros métodos de prueba:
class ProductTestSuite {
private WebDriver driver;
@BeforeEach
void setUp() {
driver = new ChromeDriver();
driver.get("https://example-shop.com/products/1");
}
@Test
void shouldDisplayCorrectProductInformation() {
var productPage = new ProductPage(driver);
assertThat(productPage.getProductTitle())
.isNotEmpty()
.contains("Premium");
}
@AfterEach
void tearDown() {
driver.quit();
}
}
Esta aproximación mantiene nuestras pruebas enfocadas en la lógica de verificación, mientras que PageFactory se encarga transparentemente de la gestión de elementos web, proporcionando una base sólida para construir suites de pruebas mantenibles y escalables.
Anotaciones @FindBy y @CacheLookup
La anotación @FindBy
La anotación @FindBy constituye el elemento central de PageFactory, permitiendo definir estrategias de localización de elementos web de forma declarativa. Esta anotación acepta los mismos localizadores que utilizaríamos con By en Selenium WebDriver tradicional, pero de manera más expresiva y mantenible.
Localizadores básicos con @FindBy:
public class SearchPage {
@FindBy(id = "search-input")
private WebElement searchField;
@FindBy(name = "category")
private WebElement categoryDropdown;
@FindBy(className = "search-button")
private WebElement searchButton;
@FindBy(tagName = "h1")
private WebElement pageTitle;
}
Localizadores avanzados con CSS y XPath
Los selectores CSS y XPath ofrecen mayor flexibilidad para elementos complejos:
public class NavigationPage {
@FindBy(css = ".navbar-nav .dropdown-toggle")
private WebElement userMenu;
@FindBy(css = "ul.menu-items li:nth-child(3)")
private WebElement thirdMenuItem;
@FindBy(xpath = "//button[contains(@class, 'primary') and text()='Submit']")
private WebElement submitButton;
@FindBy(xpath = "//div[@data-testid='notification']//span[@class='message']")
private WebElement notificationMessage;
}
Localización de listas de elementos
PageFactory también soporta la inicialización de listas de elementos mediante List<WebElement>:
public class ProductListPage {
@FindBy(css = ".product-card")
private List<WebElement> productCards;
@FindBy(xpath = "//div[@class='filter-option']//input[@type='checkbox']")
private List<WebElement> filterCheckboxes;
public int getProductCount() {
return productCards.size();
}
public List<String> getProductTitles() {
return productCards.stream()
.map(card -> card.findElement(By.className("product-title")))
.map(WebElement::getText)
.toList();
}
}
Combinando múltiples estrategias
En ocasiones necesitamos estrategias alternativas de localización. @FindBy permite definir múltiples localizadores:
public class FlexiblePage {
@FindBy(id = "submit-btn")
@FindBy(css = "button[type='submit']")
@FindBy(xpath = "//input[@value='Submit']")
private WebElement submitElement;
}
PageFactory intentará cada estrategia en orden hasta encontrar el elemento, proporcionando mayor robustez ante cambios en el DOM.
La anotación @CacheLookup
La anotación @CacheLookup es una optimización de rendimiento que instruye a PageFactory para almacenar en caché la referencia del elemento web después de la primera búsqueda. Esto resulta especialmente útil para elementos que no cambian durante la sesión del navegador.
public class HeaderPage {
@FindBy(css = ".site-logo")
@CacheLookup
private WebElement siteLogo;
@FindBy(id = "main-navigation")
@CacheLookup
private WebElement mainNavigation;
// Elemento dinámico - NO usar @CacheLookup
@FindBy(css = ".user-notifications .badge")
private WebElement notificationCount;
}
Cuándo usar @CacheLookup
El uso de @CacheLookup debe aplicarse cuidadosamente. Es apropiado para elementos estáticos como:
- Elementos de navegación que permanecen constantes
- Logos y elementos de branding
- Menús principales que no cambian
- Elementos estructurales del layout
public class StaticElementsPage {
@FindBy(id = "footer-copyright")
@CacheLookup
private WebElement copyrightText;
@FindBy(css = ".breadcrumb-container")
@CacheLookup
private WebElement breadcrumbContainer;
public String getCopyrightYear() {
return copyrightText.getText()
.replaceAll(".*?(\\d{4}).*", "$1");
}
}
Elementos que NO deben usar @CacheLookup
Evita @CacheLookup en elementos que pueden cambiar su estado o contenido:
public class DynamicContentPage {
// ❌ NO usar @CacheLookup - el contenido cambia
@FindBy(css = ".live-score")
private WebElement liveScore;
// ❌ NO usar @CacheLookup - puede aparecer/desaparecer
@FindBy(css = ".error-message")
private WebElement errorMessage;
// ❌ NO usar @CacheLookup - contenido dinámico
@FindBy(css = ".shopping-cart-count")
private WebElement cartItemCount;
}
Implementación práctica en pruebas
En el contexto de pruebas JUnit 5, la combinación de estas anotaciones proporciona un equilibrio entre rendimiento y flexibilidad:
class EcommercePageTest {
private WebDriver driver;
private ProductCatalogPage catalogPage;
@BeforeEach
void setUp() {
driver = new ChromeDriver();
catalogPage = new ProductCatalogPage(driver);
driver.get("https://example-store.com/catalog");
}
@Test
void shouldFilterProductsByCategory() {
catalogPage.selectCategory("Electronics");
catalogPage.applyFilters();
var products = catalogPage.getVisibleProducts();
assertThat(products)
.hasSizeGreaterThan(0)
.allMatch(product -> product.contains("Electronics"));
}
}
La página correspondiente aprovecha ambas anotaciones estratégicamente:
public class ProductCatalogPage {
private WebDriver driver;
@FindBy(css = ".category-sidebar")
@CacheLookup
private WebElement categorySidebar;
@FindBy(css = ".product-grid .product-item")
private List<WebElement> productItems;
@FindBy(id = "apply-filters")
private WebElement applyFiltersButton;
public ProductCatalogPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void selectCategory(String categoryName) {
var categoryOption = categorySidebar.findElement(
By.xpath(String.format(".//input[@value='%s']", categoryName))
);
categoryOption.click();
}
public List<String> getVisibleProducts() {
return productItems.stream()
.filter(WebElement::isDisplayed)
.map(item -> item.findElement(By.className("product-name")))
.map(WebElement::getText)
.toList();
}
}
Esta aproximación optimiza el rendimiento mediante el cacheo de elementos estáticos como la barra lateral de categorías, mientras mantiene la flexibilidad necesaria para elementos dinámicos como la lista de productos que cambia según los filtros aplicados.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Selenium
Documentación oficial de Selenium
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Selenium es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Selenium
Explora más contenido relacionado con Selenium y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender el patrón PageFactory y su diferencia con el Page Object tradicional.
- Aprender a utilizar la anotación @FindBy para localizar elementos web de forma declarativa.
- Conocer el funcionamiento del método PageFactory.initElements() y el patrón proxy para la búsqueda diferida de elementos.
- Identificar cuándo y cómo usar la anotación @CacheLookup para optimizar el rendimiento.
- Integrar PageFactory con JUnit 5 para construir pruebas automatizadas mantenibles y escalables.