Page Object Model con Python

Avanzado
Selenium
Selenium
Actualizado: 05/05/2026

El patrón Page Object Model en Python

flowchart TB
    Tests["tests/test_*.py pytest"] --> Pages["pages/"]
    Pages --> BP[base_page.py BasePage class]
    Pages --> LP[login_page.py LoginPage]
    Pages --> HP[home_page.py HomePage]
    Pages --> CP[cart_page.py CartPage]
    BP --> Driver["self.driver: WebDriver"]
    BP --> Wait["self.wait: WebDriverWait"]
    BP --> Find[wait_until_visible locator]
    LP --> Loc["Locators tuples by, value"]
    LP --> Met["Métodos: login, fill_user"]
    Met --> Ret[Return HomePage instance]
    Tests --> Conf[conftest.py fixture driver]
    Conf --> Yield["yield driver / driver.quit"]

El Page Object Model (POM) es el patrón de diseño más utilizado en la automatización web. En Python, su implementación es especialmente elegante gracias al tipado con typing y la claridad de la sintaxis.

La idea central es encapsular en clases los localizadores y las interacciones de cada página, separando la lógica de los tests de los detalles técnicos de la interfaz.

Sin POM:

# Frágil: localizadores esparcidos por todo el código
def test_login():
    driver.find_element(By.ID, "email").send_keys("user@test.com")
    driver.find_element(By.ID, "password").send_keys("pass123")
    driver.find_element(By.CSS_SELECTOR, ".btn-login").click()
    assert "/dashboard" in driver.current_url

Con POM:

# Limpio: la lógica de UI está en LoginPage
def test_login(driver):
    login = LoginPage(driver)
    dashboard = login.login_como("user@test.com", "pass123")
    assert dashboard.esta_cargada()

BasePage: clase base con utilidades comunes

Crear una BasePage con métodos de espera y utilidades comunes evita duplicar código en cada Page Object.

# pages/base_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement


class BasePage:
    """Clase base con métodos de utilidad para todos los Page Objects."""

    TIMEOUT = 10  # Tiempo máximo de espera en segundos

    def __init__(self, driver: WebDriver):
        self.driver = driver
        self.espera = WebDriverWait(driver, self.TIMEOUT)

    def ir_a(self, url: str) -> None:
        """Navega a la URL especificada."""
        self.driver.get(url)

    def esperar_visible(self, localizador: tuple) -> WebElement:
        """Espera hasta que el elemento sea visible y lo devuelve."""
        return self.espera.until(
            EC.visibility_of_element_located(localizador)
        )

    def esperar_clickable(self, localizador: tuple) -> WebElement:
        """Espera hasta que el elemento sea clickable y lo devuelve."""
        return self.espera.until(
            EC.element_to_be_clickable(localizador)
        )

    def esperar_url_contiene(self, texto: str) -> bool:
        """Espera hasta que la URL contenga el texto especificado."""
        return self.espera.until(EC.url_contains(texto))

    def obtener_titulo(self) -> str:
        """Devuelve el título de la página actual."""
        return self.driver.title

    def obtener_url_actual(self) -> str:
        """Devuelve la URL de la página actual."""
        return self.driver.current_url

    def tomar_captura(self, nombre_archivo: str) -> None:
        """Toma una captura de pantalla."""
        self.driver.save_screenshot(nombre_archivo)

    def esta_en_pagina(self, url_esperada: str) -> bool:
        """Verifica si la URL actual contiene la esperada."""
        return url_esperada in self.driver.current_url

Page Objects concretos

# pages/login_page.py
from __future__ import annotations
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class LoginPage(BasePage):
    """Page Object para la página de inicio de sesión."""

    URL = "https://www.ejemplo.com/login"

    # Localizadores (privados por convención)
    _EMAIL_INPUT = (By.ID, "email")
    _PASSWORD_INPUT = (By.ID, "password")
    _LOGIN_BUTTON = (By.CSS_SELECTOR, "[data-testid='btn-login']")
    _ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
    _FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "¿Olvidaste tu contraseña?")

    def abrir(self) -> LoginPage:
        """Navega a la página de login."""
        self.ir_a(self.URL)
        return self

    def introducir_email(self, email: str) -> LoginPage:
        """Escribe el email en el campo correspondiente."""
        campo = self.esperar_clickable(self._EMAIL_INPUT)
        campo.clear()
        campo.send_keys(email)
        return self

    def introducir_password(self, password: str) -> LoginPage:
        """Escribe la contraseña en el campo correspondiente."""
        campo = self.esperar_clickable(self._PASSWORD_INPUT)
        campo.clear()
        campo.send_keys(password)
        return self

    def hacer_clic_login(self) -> DashboardPage:
        """Hace clic en el botón de login y navega al dashboard."""
        from pages.dashboard_page import DashboardPage
        self.esperar_clickable(self._LOGIN_BUTTON).click()
        return DashboardPage(self.driver)

    def login_como(self, email: str, password: str) -> DashboardPage:
        """Método conveniente: hace el login completo en un paso."""
        return (
            self.introducir_email(email)
                .introducir_password(password)
                .hacer_clic_login()
        )

    def obtener_mensaje_error(self) -> str:
        """Devuelve el texto del mensaje de error si es visible."""
        return self.esperar_visible(self._ERROR_MESSAGE).text

    def ir_a_recuperar_password(self) -> RecuperarPasswordPage:
        """Hace clic en el enlace de recuperación de contraseña."""
        from pages.recuperar_password_page import RecuperarPasswordPage
        self.esperar_clickable(self._FORGOT_PASSWORD_LINK).click()
        return RecuperarPasswordPage(self.driver)
# pages/dashboard_page.py
from __future__ import annotations
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class DashboardPage(BasePage):
    """Page Object para la página de dashboard."""

    _BIENVENIDA_TITULO = (By.CSS_SELECTOR, "h1.dashboard-title")
    _MENU_USUARIO = (By.CSS_SELECTOR, "[data-testid='user-menu']")
    _CERRAR_SESION = (By.LINK_TEXT, "Cerrar sesión")
    _CONTADOR_NOTIFICACIONES = (By.CLASS_NAME, "notification-count")

    def esta_cargada(self) -> bool:
        """Verifica que el dashboard está completamente cargado."""
        return (
            "/dashboard" in self.driver.current_url
            and self.driver.find_elements(*self._BIENVENIDA_TITULO) != []
        )

    def obtener_titulo_bienvenida(self) -> str:
        """Devuelve el título de bienvenida del dashboard."""
        return self.esperar_visible(self._BIENVENIDA_TITULO).text

    def obtener_notificaciones(self) -> int:
        """Devuelve el número de notificaciones no leídas."""
        elementos = self.driver.find_elements(*self._CONTADOR_NOTIFICACIONES)
        if not elementos:
            return 0
        return int(elementos[0].text)

    def cerrar_sesion(self) -> LoginPage:
        """Cierra la sesión y devuelve la página de login."""
        from pages.login_page import LoginPage
        self.esperar_clickable(self._MENU_USUARIO).click()
        self.esperar_clickable(self._CERRAR_SESION).click()
        return LoginPage(self.driver)

Integración con pytest

# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage


@pytest.fixture(scope="function")
def driver():
    options = Options()
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    d = webdriver.Chrome(options=options)
    d.maximize_window()
    yield d
    d.quit()


@pytest.fixture
def login_page(driver):
    """Fixture que devuelve una instancia de LoginPage lista para usar."""
    page = LoginPage(driver)
    page.abrir()
    return page


@pytest.fixture
def dashboard_page(driver):
    """Fixture que hace login y devuelve el DashboardPage."""
    login = LoginPage(driver)
    return login.abrir().login_como("admin@ejemplo.com", "admin123")
# tests/test_autenticacion.py
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage


class TestLogin:
    """Suite de tests para el módulo de autenticación."""

    def test_login_exitoso(self, login_page: LoginPage):
        """Un usuario válido puede iniciar sesión."""
        dashboard = login_page.login_como("usuario@test.com", "contraseña123")
        assert dashboard.esta_cargada()
        assert "Bienvenido" in dashboard.obtener_titulo_bienvenida()

    def test_login_con_email_incorrecto(self, login_page: LoginPage):
        """El sistema rechaza credenciales incorrectas."""
        login_page.introducir_email("incorrecto@test.com")
        login_page.introducir_password("contraseña123")
        login_page.hacer_clic_login()

        # No navegó al dashboard, hay mensaje de error
        assert "/dashboard" not in login_page.obtener_url_actual()
        assert login_page.obtener_mensaje_error() != ""

    @pytest.mark.parametrize("email,password", [
        ("", "contraseña"),
        ("email@test.com", ""),
        ("no_es_email", "contraseña"),
    ])
    def test_validaciones_formulario_login(
        self, login_page: LoginPage, email: str, password: str
    ):
        """El formulario valida los campos antes de enviar."""
        if email:
            login_page.introducir_email(email)
        if password:
            login_page.introducir_password(password)
        login_page.hacer_clic_login()

        assert "/dashboard" not in login_page.obtener_url_actual()

    def test_flujo_completo_login_logout(self, login_page: LoginPage):
        """Un usuario puede hacer login y luego logout correctamente."""
        dashboard = login_page.login_como("usuario@test.com", "contraseña123")
        assert dashboard.esta_cargada()

        login_vacia = dashboard.cerrar_sesion()
        assert login_vacia.esta_en_pagina("/login")

Estructura de proyecto recomendada

mi-suite-selenium/
├── pages/
│   ├── __init__.py
│   ├── base_page.py          # Clase base con utilidades comunes
│   ├── login_page.py
│   ├── dashboard_page.py
│   ├── registro_page.py
│   └── catalogo_page.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Fixtures compartidas
│   ├── test_autenticacion.py
│   ├── test_catalogo.py
│   └── test_checkout.py
├── utils/
│   ├── __init__.py
│   └── datos_prueba.py       # Datos de prueba y constantes
├── reportes/                  # Generado por pytest-html
├── capturas/                  # Capturas en fallos
├── requirements.txt
└── pytest.ini

Buenas prácticas del POM en Python

# BIEN: método expresivo que describe la acción del usuario
def completar_registro(self, nombre, email, password):
    self.introducir_nombre(nombre)
    self.introducir_email(email)
    self.introducir_password(password)
    return self.confirmar_registro()

# BIEN: sin aserciones en Page Objects
def obtener_mensaje_exito(self) -> str:
    return self.esperar_visible(self._MENSAJE_EXITO).text

# MAL: aserción en Page Object (va en el test)
# def verificar_registro_exitoso(self):
#     assert self.esperar_visible(self._MENSAJE_EXITO).text == "Registro completado"

# BIEN: type hints para mejor experiencia de desarrollo
def login_como(self, email: str, password: str) -> DashboardPage:
    ...

# BIEN: localizadores como constantes de clase
_BOTON_ENVIAR = (By.CSS_SELECTOR, "[data-testid='submit']")

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Selenium

Documentación oficial de Selenium
Alan Sastre - Autor del tutorial

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

Crear clases Page Object en Python que encapsulen localizadores y acciones. Implementar el flujo de navegación entre páginas retornando nuevas instancias POM. Organizar un proyecto Selenium Python con POM, utils y tests bien estructurados. Combinar Page Object Model con fixtures de pytest para pruebas limpias. Aplicar buenas prácticas: sin aserciones en Page Objects, métodos expresivos y tipado. Implementar una BasePage con métodos comunes reutilizables en todos los Page Objects.