Selenium + Python: el arte de los selectores que no se rompen
Selenium sigue moviendo una parte enorme de la automatización mundial, especialmente en banca, retail y sistemas legados. Y la causa número uno de suites Selenium frágiles sigue siendo la misma: selectores mal elegidos y esperas mal entendidas.
La jerarquía de selectores
De más a menos robusto:
# 1. ID estable (si tu equipo los mantiene)
driver.find_element(By.ID, "submit-booking")
# 2. Atributo de test dedicado
driver.find_element(By.CSS_SELECTOR, "[data-test='submit-booking']")
# 3. Texto visible vía XPath (se rompe solo si cambia el copy)
driver.find_element(By.XPATH, "//button[normalize-space()='Reservar hora']")
# 4. CSS semántico corto
driver.find_element(By.CSS_SELECTOR, "form.booking button[type='submit']")
Y lo que nunca deberías commitear:
# ❌ El XPath absoluto: se rompe si alguien agrega un div
driver.find_element(By.XPATH, "/html/body/div[2]/div/div[3]/form/div[4]/button")
Si tu grabador de tests genera esto, el grabador es para prototipar, no para commitear.
Esperas: implícitas vs explícitas
El error clásico es mezclarlas. La espera implícita global más esperas explícitas produce timeouts impredecibles. Regla simple: implícita en 0, explícitas siempre:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
boton = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-test='submit']")))
boton.click()
confirmacion = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".alert-success")))
time.sleep() en un test es deuda técnica con intereses: cada segundo dormido se paga en cada ejecución, para siempre.
Page Object Model sin dogma
El POM funciona cuando encapsula comportamiento, no cuando es un espejo del DOM:
class BookingPage:
URL = "/reservas"
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def reservar(self, servicio: str, hora: str) -> "ConfirmationPage":
self._select_servicio(servicio)
self._select_hora(hora)
self.wait.until(EC.element_to_be_clickable(self.SUBMIT)).click()
return ConfirmationPage(self.driver)
El test queda legible como un caso de negocio:
def test_reserva_corte_de_pelo(driver):
confirmacion = BookingPage(driver).open().reservar("Corte", "15:30")
assert confirmacion.mensaje() == "¡Tu hora está reservada!"
Checklist antes de dar por buena tu suite
- Cero XPath absolutos y cero
time.sleep(). - Cada selector sobrevive a un cambio de CSS puro.
- Los page objects devuelven page objects (navegación tipada).
- La suite corre en paralelo con pytest-xdist sin pisarse datos.
Para partir desde cero: el curso gratuito de Selenium con Python construye todo esto paso a paso, con un proyecto real.