
¿Por qué pytest con Selenium?
pytest es el framework de testing más utilizado en Python gracias a su potencia, flexibilidad y la enorme cantidad de plugins disponibles. Comparado con unittest, pytest ofrece:
- Fixtures para gestionar recursos compartidos entre tests
- Parametrización para ejecutar el mismo test con distintos datos
- Plugins para reportes, cobertura, ejecución paralela, etc.
- Sintaxis más concisa y legible
- Mejor gestión de errores y mensajes de fallo informativos
Instalación de pytest y plugins
pip install pytest
pip install pytest-html # Reportes HTML
pip install pytest-xdist # Ejecución paralela
pip install pytest-selenium # Plugin oficial Selenium (opcional)
Primer test con pytest y Selenium
# tests/test_basico.py
from selenium import webdriver
from selenium.webdriver.common.by import By
def test_titulo_pagina_selenium():
"""Verifica que el título de la página contiene 'Selenium'."""
with webdriver.Chrome() as driver:
driver.get("https://www.selenium.dev")
assert "Selenium" in driver.title
def test_pagina_documentacion_accesible():
"""Verifica que la documentación de Selenium es accesible."""
with webdriver.Chrome() as driver:
driver.get("https://www.selenium.dev/documentation/")
assert driver.current_url == "https://www.selenium.dev/documentation/"
assert "Selenium" in driver.title
Ejecutar los tests:
# Ejecutar todos los tests
pytest tests/
# Ejecutar con salida detallada
pytest tests/ -v
# Ejecutar un test específico
pytest tests/test_basico.py::test_titulo_pagina_selenium
Fixtures: gestión del ciclo de vida del WebDriver
Las fixtures son la forma recomendada de gestionar el WebDriver en pytest. Garantizan que el navegador se cierre correctamente incluso si el test falla.
# tests/test_con_fixture.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
@pytest.fixture
def driver():
"""Fixture que crea y cierra el WebDriver para cada test."""
chrome_driver = webdriver.Chrome()
chrome_driver.maximize_window()
yield chrome_driver # El test usa este driver
chrome_driver.quit() # Se ejecuta SIEMPRE al final, incluso si el test falla
def test_navegacion(driver):
"""Usa la fixture 'driver' automáticamente."""
driver.get("https://www.selenium.dev")
assert "Selenium" in driver.title
def test_titulo(driver):
"""Cada test recibe una instancia nueva del driver."""
driver.get("https://www.selenium.dev/documentation/")
assert driver.find_element(By.TAG_NAME, "h1").text != ""
Alcance de las fixtures (scope)
import pytest
from selenium import webdriver
@pytest.fixture(scope="function") # Una instancia por test (default)
def driver_por_test():
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="class") # Una instancia por clase de test
def driver_por_clase():
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="module") # Una instancia por módulo (archivo .py)
def driver_por_modulo():
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="session") # Una instancia para toda la sesión de pytest
def driver_sesion():
driver = webdriver.Chrome()
yield driver
driver.quit()
conftest.py: fixtures compartidas
El archivo conftest.py es descubierto automáticamente por pytest y permite compartir fixtures entre múltiples archivos de test sin importarlas explícitamente.
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
@pytest.fixture(scope="session")
def base_url():
"""URL base de la aplicación bajo prueba."""
return "https://www.ejemplo.com"
@pytest.fixture
def driver(request):
"""Fixture principal del WebDriver, configurable por parámetro."""
navegador = request.param if hasattr(request, "param") else "chrome"
if navegador == "chrome":
options = ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
d = webdriver.Chrome(options=options)
elif navegador == "firefox":
d = webdriver.Firefox()
elif navegador == "edge":
d = webdriver.Edge()
else:
raise ValueError(f"Navegador no soportado: {navegador}")
d.maximize_window()
d.implicitly_wait(5)
yield d
d.quit()
@pytest.fixture
def driver_headless():
"""WebDriver en modo headless para CI/CD."""
options = ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--window-size=1920,1080")
d = webdriver.Chrome(options=options)
yield d
d.quit()
Parametrización cross-browser
# tests/test_cross_browser.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
def crear_driver(navegador):
if navegador == "chrome":
return webdriver.Chrome()
elif navegador == "firefox":
return webdriver.Firefox()
elif navegador == "edge":
return webdriver.Edge()
@pytest.fixture(params=["chrome", "firefox", "edge"])
def driver_multinavegador(request):
"""Fixture que ejecuta el test en 3 navegadores distintos."""
d = crear_driver(request.param)
d.maximize_window()
yield d
d.quit()
def test_titulo_en_todos_los_navegadores(driver_multinavegador):
"""Este test se ejecuta 3 veces: Chrome, Firefox y Edge."""
driver_multinavegador.get("https://www.selenium.dev")
assert "Selenium" in driver_multinavegador.title
Marcadores y agrupación de tests
# tests/test_marcadores.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
@pytest.mark.humo
def test_pagina_principal_carga(driver):
"""Test de humo: verifica que la página carga."""
driver.get("https://www.ejemplo.com")
assert driver.title != ""
@pytest.mark.regresion
def test_login_correcto(driver, base_url):
"""Test de regresión para el flujo de login."""
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "email").send_keys("usuario@test.com")
driver.find_element(By.ID, "password").send_keys("contraseña")
driver.find_element(By.ID, "btn-login").click()
WebDriverWait(driver, 10).until(
EC.url_contains("/dashboard")
)
assert "/dashboard" in driver.current_url
@pytest.mark.slow
def test_carga_completa_catalogo(driver, base_url):
"""Test lento: carga el catálogo completo."""
driver.get(f"{base_url}/catalogo")
# ...
Ejecutar solo tests con un marcador:
pytest -m humo
pytest -m "not slow"
pytest -m "humo or regresion"
Configuración de pytest.ini
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --html=reportes/reporte.html --self-contained-html
markers =
humo: Tests de humo básicos
regresion: Tests de regresión completos
slow: Tests que tardan más de 30 segundos
cross_browser: Tests que se ejecutan en múltiples navegadores
Reportes HTML con pytest-html
# Generar reporte HTML
pytest tests/ --html=reportes/reporte.html --self-contained-html
# Reporte con metadatos adicionales
pytest tests/ --html=reportes/reporte.html --self-contained-html --metadata Entorno Staging --metadata Versión 2.1.0
Para adjuntar capturas de pantalla al reporte cuando un test falla:
# tests/conftest.py
import pytest
from pathlib import Path
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
if rep.when == "call" and rep.failed:
# Adjuntar captura de pantalla al reporte
driver = item.funcargs.get("driver")
if driver:
captura_path = Path("reportes") / f"fallo_{item.name}.png"
captura_path.parent.mkdir(exist_ok=True)
driver.save_screenshot(str(captura_path))
if hasattr(rep, "extras"):
from pytest_html import extras
rep.extras.append(extras.image(str(captura_path)))
Ejecución paralela con pytest-xdist
# Ejecutar en paralelo usando tantos cores como tenga la máquina
pytest tests/ -n auto
# Usar exactamente 4 workers
pytest tests/ -n 4
# Distribuir por archivo (cada worker ejecuta un archivo completo)
pytest tests/ -n 4 --dist loadfile
Para que la ejecución paralela funcione, cada test necesita su propio driver:
# conftest.py para ejecución paralela
@pytest.fixture(scope="function") # Siempre scope="function" para xdist
def driver():
d = webdriver.Chrome()
yield d
d.quit()
Ejemplo completo de suite de pruebas
# tests/test_suite_completa.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestAutenticacion:
"""Suite de tests para el módulo de autenticación."""
def test_login_exitoso(self, driver, base_url):
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "email").send_keys("admin@ejemplo.com")
driver.find_element(By.ID, "password").send_keys("admin123")
driver.find_element(By.ID, "btn-login").click()
WebDriverWait(driver, 10).until(EC.url_contains("/dashboard"))
assert "/dashboard" in driver.current_url
def test_login_credenciales_incorrectas(self, driver, base_url):
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "email").send_keys("invalido@ejemplo.com")
driver.find_element(By.ID, "password").send_keys("contraseñamala")
driver.find_element(By.ID, "btn-login").click()
mensaje_error = WebDriverWait(driver, 5).until(
EC.visibility_of_element_located((By.CLASS_NAME, "error-message"))
)
assert "credenciales" in mensaje_error.text.lower()
@pytest.mark.parametrize("email,password", [
("", "contraseña"),
("email@test.com", ""),
("", ""),
("no-es-email", "contraseña"),
])
def test_validacion_formulario_login(self, driver, base_url, email, password):
driver.get(f"{base_url}/login")
if email:
driver.find_element(By.ID, "email").send_keys(email)
if password:
driver.find_element(By.ID, "password").send_keys(password)
driver.find_element(By.ID, "btn-login").click()
# Verificar que NO navegó al dashboard
assert "/dashboard" not in driver.current_url
Árbol de decisión del framework de tests
Selenium se integra de forma idiomática con varios frameworks de tests. La elección depende del lenguaje del equipo y de las funcionalidades de paralelismo, fixtures y reportes que se necesiten. El siguiente árbol orienta la decisión.
flowchart TD
A[Equipo va a usar Selenium] --> B{"¿Lenguaje principal del equipo?"}
B -- Java --> C{"¿Necesita data driven o paralelismo nativo?"}
C -- Sí --> D[TestNG con DataProvider y parallel]
C -- No --> E[JUnit 5 Jupiter con assertJ]
B -- Python --> F[Pytest con fixtures y parametrize]
B -- C Sharp --> G[NUnit o xUnit]
B -- JavaScript --> H[Mocha o Jest con webdriverio]
D --> I[Reportes Allure compatibles]
E --> I
F --> I
G --> I
H --> I
Para 2026 la combinación más extendida en proyectos Java es JUnit 5 + Selenium 4 + Selenide o Page Factory. Pytest sigue siendo la elección dominante en Python por su sintaxis ligera, sus fixtures y la integración con Allure.
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
Configurar fixtures de pytest para gestionar el ciclo de vida del WebDriver. Usar conftest.py para compartir fixtures entre múltiples archivos de test. Parametrizar pruebas para ejecutarlas en múltiples navegadores con pytest.mark.parametrize. Instalar y usar pytest-html para generar reportes HTML de las pruebas. Ejecutar pruebas en paralelo con pytest-xdist para reducir el tiempo de ejecución. Configurar pytest.ini y pyproject.toml para personalizar el comportamiento de pytest.