Fundamentos del Patrón Page Object Model
Incrustrar selectores y acciones de interfaz directamente dentro de las funciones de prueba es viable para prototipos rápidos, pero se convierte en una carga técnica insostenible cuando la suite crece. Cualquier modificación en el DOM, como un cambio de atributo class o un ajuste en la jerarquía de nodos, obliga a refactorizar decenas de archivos. El patrón Page Object Model (POM) resuelve este acoplamiento mediante la separación estricta de responsabilidades: la estructura visual y las interacciones se encapsulan en clases dedicadas, mientras que los tests se limitan a orquestar flujos de negocio y validar resultados.
Estructura de Directorios Recomendada
suite_automatizacion/
├── conftest.py # Configuración global y fixtures
├── pytest.ini
├── paginas/ # Capa de abstracción de UI
│ ├── __init__.py
│ ├── base.py # Funcionalidades transversales
│ ├── acceso.py # Pantalla de login
│ └── panel.py # Vista post-autenticación
├── fragmentos/ # UI compartida entre múltiples vistas
│ └── barra_lateral.py
└── escenarios/ # Casos de prueba puros
└── test_autenticacion.py
Implementación de la Clase Base
La clase fundacional centraliza operaciones repetitivas como navegación, captura de estado y utilidades de sincronización. Esto evita duplicación y estandariza el comportamiento de todas las páginas derivadas.
# paginas/base.py
from playwright.sync_api import Page
class VistaBase:
RUTA_DESTINO = None
def __init__(self, controlador: Page):
self.navegador = controlador
def ir_a(self, url_personalizada: str = None):
destino = url_personalizada or self.RUTA_DESTINO
if destino:
self.navegador.goto(destino)
return self
def titulo_actual(self) -> str:
return self.navegador.title()
def guardar_evidencia(self, nombre_archivo: str):
self.navegador.screenshot(path=f"evidencias/{nombre_archivo}.png")
return self
Definición de Páginas Específicas
Cada vista se modela como una entidad independiente. Los localizadores se exponen como propiedades de solo lectura y las acciones devuelven referencias a objetos, habilitando una interfaz fluida.
# paginas/acceso.py
from playwright.sync_api import Page
from paginas.base import VistaBase
from paginas.panel import PanelControl
class PantallaAcceso(VistaBase):
RUTA_DESTINO = "https://app.demo/login"
@property
def input_identificador(self):
return self.navegador.get_by_placeholder("Usuario o email")
@property
def input_secreto(self):
return self.navegador.get_by_placeholder("Clave de acceso")
@property
def btn_confirmar(self):
return self.navegador.get_by_role("button", name="Iniciar sesión")
@property
def aviso_fallo(self):
return self.navegador.locator("div.alerta-error")
def ingresar_credenciales(self, usuario: str, clave: str):
self.input_identificador.fill(usuario)
self.input_secreto.fill(clave)
return self
def ejecutar_login(self) -> PanelControl:
self.btn_confirmar.click()
return PanelControl(self.navegador)
def autenticar_completo(self, usr: str, pwd: str) -> PanelControl:
self.ir_a()
return self.ingresar_credenciales(usr, pwd).ejecutar_login()
# paginas/panel.py
from playwright.sync_api import Page
from paginas.base import VistaBase
class PanelControl(VistaBase):
RUTA_DESTINO = "https://app.demo/dashboard"
@property
def etiqueta_bienvenida(self):
return self.navegador.get_by_text("Hola,")
@property
def enlace_salir(self):
return self.navegador.get_by_role("link", name="Cerrar sesión")
def obtener_nombre_sesion(self) -> str:
return self.etiqueta_bienvenida.inner_text()
Capa de Pruebas y Ejecución
Los archivos de test nunca deben contener selectores ni llamadas directas al DOM. Su único propósito es describir el comportamiento esperado del sistema y verificar los estados resultantes.
# escenarios/test_autenticacion.py
import pytest
from playwright.sync_api import Page, expect
from paginas.acceso import PantallaAcceso
@pytest.fixture
def vista_login(page: Page) -> PantallaAcceso:
return PantallaAcceso(page)
def test_acceso_correcto_redirige_al_panel(vista_login):
credenciales = {"u": "coord_admin", "p": "Str0ngP@ss!"}
panel = vista_login.autenticar_completo(credenciales["u"], credenciales["p"])
expect(panel.navegador).to_have_url("**/dashboard")
assert "coord_admin" in panel.obtener_nombre_sesion()
def test_credenciales_invalidas_muestran_alerta(vista_login):
vista_login.ir_a().ingresar_credenciales("coord_admin", "clave_erronea").ejecutar_login()
expect(vista_login.aviso_fallo).to_be_visible()
expect(vista_login.aviso_fallo).to_contain_text("Autenticación fallida")
Extracción de Componentes Reutilizables
Elementos que persisten a través de múltiples rutas (menús, modales globales, pies de página) deben desacoplarse en fragmentos independientes. Estas clases se componen dentro de las páginas que los requieren.
# fragmentos/barra_lateral.py
from playwright.sync_api import Page
class MenuLateral:
def __init__(self, contexto: Page):
self.ctx = contexto
def seleccionar_modulo(self, nombre_modulo: str):
self.ctx.get_by_role("link", name=nombre_modulo).click()
return self
# Integración en paginas/panel.py
class PanelControl(VistaBase):
def __init__(self, controlador: Page):
super().__init__(controlador)
self.menu = MenuLateral(controlador)
Uso en pruebas: panel.menu.seleccionar_modulo("Reportes"). La lógica de navegación se mantiene en un único punto de verdad.
Inyección de Dependencias con pytest
El archivo conftest.py actúa como proveedor centralizado. Al registrar las páginas como fixtures, se elimina la necesidad de instanciación manual dentro de cada test.
# conftest.py
import pytest
from paginas.acceso import PantallaAcceso
from paginas.panel import PanelControl
@pytest.fixture
def ui_acceso(page):
return PantallaAcceso(page)
@pytest.fixture
def ui_panel(page):
return PanelControl(page)
Directrices de Arquitectura y Errores Comunes
| Práctica Recomendada | Antipatrón a Evitar |
|---|---|
| Localizadores aislados en propiedades de la clase | Selectores hardcodeados dentro de los asserts o pasos del test |
Métodos que retornan self o la siguiente página |
Funciones void que rompen la cadena de llamadas |
Nombres orientados al dominio (autenticar_completo) |
Operaciones atómicas expuestas (click_boton, llenar_campo) |
Validaciones (expect/assert) exclusivas del test |
Lógica de verificación mezclada dentro de los Page Objects |
Uso de selectores semánticos (get_by_role, get_by_text) |
XPath profundos o selectores CSS dependientes de la maquetación |
Cuándo Aplicar Esta Arquitectura
Para colecciones menores a cinco casos, un script lineal es suficiente. Entre diez y varios cientos de escenarios, POM reduce drásticamente la deuda técnica al confinar los cambios de UI a una sola capa. En ecosistemas corporativos con interfaces moudlares complejas, la combinación de POM + Fragmentos compone la estrategia más equilibrada entre legibilidad y esfuerzo de mantenimiento. Patrones más abstractos como Screenplay solo se justifican cuando múltiples roles de usuario interactúan con flujos altamente dinámicos y la curva de aprendizaje está justificada por la escala del proyecto.