Las pruebas automatizadas son fundamentales para garantizar la calidad y la confiabilidad del código Python. Entre las diversas herramientas disponibles, pytest se destaca como la opción preferida de los desarrolladores de Python en todo el mundo. Esta guía completa te mostrará todo lo que necesitas saber para dominar las pruebas automatizadas con pytest en tus proyectos.
Por Qué Utilizar pytest para Pruebas en Python?
pytest es un framework de pruebas moderno que ofrece un enfoque simple y poderoso para escribir pruebas en Python. A diferencia de otras herramientas, pytest minimiza la cantidad de código necesaria para escribir pruebas claras y eficientes. La filosofía de pytest es "pruebas simples y legibles", haciéndolo accesible tanto para principiantes como para desarrolladores experimentados.
Entre las principales ventajas de pytest, podemos destacar su sintaxis concisa que permite crear pruebas con menos líneas de código. El descubrimiento automático de pruebas es otra característica poderosa donde el framework encuentra y ejecuta pruebas sin necesidad de configuración compleja. Además, pytest posee una vasta colección de plugins que extienden sus funcionalidades, permitiendo integración con herramientas de coverage, profiling y mucho más.
pytest también ofrece mensajes de error extremadamente detallados y útiles, facilitando significativamente el proceso de debugging. Cuando una prueba falla, pytest muestra exactamente qué afirmación falló y en qué línea del código, haciendo la corrección de errores mucho más rápida y eficiente.
Instalación y Configuración de pytest
La instalación de pytest es extremadamente simple y puede realizarse a través de pip, el administrador de paquetes de Python. Ejecuta el siguiente comando en tu terminal:
pip install pytest
Para verificar si la instalación fue exitosa, puedes ejecutar:
pytest --version
Este comando debe devolver la versión de pytest instalada, confirmando que todo está funcionando correctamente. Es muy recomendado crear un entorno virtual específico para tu proyecto, aislar tus dependencias y garantizar que las pruebas se ejecuten de forma consistente en diferentes entornos.
Si necesitas funcionalidades adicionales, puedes instalar plugins específicos. Por ejemplo, para pruebas de coverage de código, puedes instalar pytest-cov:
pip install pytest-cov
Otro plugin muy útil es pytest-xdist, que permite ejecutar pruebas en paralelo, reduciendo significativamente el tiempo de ejecución de tu suite de pruebas:
pip install pytest-xdist
Tu Primera Suite de Pruebas con pytest
Ahora que pytest está instalado, vamos a crear nuestra primera prueba. pytest sigue convenciones simples: los archivos de prueba deben comenzar con "test_" o terminar con "test.py". Las funciones de prueba también deben comenzar con "test".
Vamos a crear un archivo de ejemplo llamado test_matematica.py:
def sumar(a, b):
return a + b
def restar(a, b):
return a - b
def multiplicar(a, b):
return a * b
def dividir(a, b):
if b == 0:
raise ValueError("División por cero no está permitida")
return a / b
## Pruebas
def test_sumar():
assert sumar(2, 3) == 5
assert sumar(-1, 1) == 0
assert sumar(0, 0) == 0
def test_restar():
assert restar(5, 3) == 2
assert restar(10, 10) == 0
assert restar(-5, -3) == -2
def test_multiplicar():
assert multiplicar(3, 4) == 12
assert multiplicar(0, 5) == 0
assert multiplicar(-2, 3) == -6
def test_dividir():
assert dividir(10, 2) == 5
assert dividir(15, 3) == 5
assert dividir(100, 10) == 10
def test_dividir_por_cero():
with pytest.raises(ValueError):
dividir(10, 0)
Para ejecutar las pruebas, solo necesitas ejecutar el comando pytest en el directorio de tu proyecto o especificar el archivo:
pytest test_matematica.py
pytest descubrirá automáticamente y ejecutará todas las funciones que comienzan con "test_" y mostrará resultados detallados de las pruebas. La salida incluirá el número de pruebas que pasaron y fallaron, junto con información detallada de las pruebas fallidas.
Aserciones con pytest
pytest ofrece diversas formas de realizar aserciones, siendo la más directa el uso del comando assert. Cuando una aserción falla, pytest proporciona información detallada sobre el error, incluyendo el valor esperado y el valor obtenido.
Además del assert estándar, pytest también proporciona módulos auxiliares que ofrecen aserciones más específicas. El módulo pytest contiene diversas funciones que facilitan pruebas comunes:
import pytest
def test_verificar_igualdad():
assert 5 == 5
def test_verificar_verdadero():
assert True
def test_verificar_falso():
assert not False
def test_verificar_none():
resultado = None
assert resultado is None
def test_verificar_contiene():
lista = [1, 2, 3, 4, 5]
assert 3 in lista
def test_verificar_tamano():
texto = "Python"
assert len(texto) == 6
def test_verificar_mayor_menor():
assert 10 > 5
assert 3 < 8
Para aserciones más complejas, puedes usar las funciones auxiliares de pytest. Por ejemplo, pytest.approx() es útil para comparaciones de números flotantes, donde necesitas manejar imprecisiones de punto flotante:
def test_calculo_decimal():
resultado = 0.1 + 0.2
assert resultado == pytest.approx(0.3, rel=1e-2)
Fixtures en pytest
Los fixtures son una de las funcionalidades más poderosas de pytest. Permiten crear datos de prueba reutilizables y gestionar dependencias de forma elegante. Un fixture se define usando el decorador @pytest.fixture y puede ser inyectado en cualquier función de prueba.
Veamos un ejemplo práctico:
import pytest
## Fixture simple que retorna un diccionario de usuario
@pytest.fixture
def usuario():
return {
'nombre': 'Juan Pérez',
'email': '[email protected]',
'edad': 30
}
## Fixture que crea una conexión de base de datos simulada
@pytest.fixture
def conexion_db():
# Simula la conexión con una base de datos
conexion = {
'conectado': False,
'datos': []
}
def ejecutar_query(query):
conexion['datos'].append(query)
return ['resultado_simulado']
conexion['ejecutar_query'] = ejecutar_query
return conexion
## Pruebas utilizando los fixtures
def test_usuario_nombre(usuario):
assert usuario['nombre'] == 'Juan Pérez'
def test_usuario_email(usuario):
assert '@' in usuario['email']
def test_conexion_db(conexion_db):
resultado = conexion_db['ejecutar_query']("SELECT * FROM usuarios")
assert len(resultado) > 0
assert len(conexion_db['datos']) == 1
Los fixtures pueden tener diferentes alcances, determinando con qué frecuencia se crean. El alcance por defecto es "function", lo que significa que un fixture se crea una vez por función de prueba. Puedes cambiar el alcance a "module", "class" o "session" según sea necesario:
## Fixture de alcance de módulo - creada una vez por archivo de prueba
@pytest.fixture(scope="module")
def configuracion_global():
return {
'ambiente': 'desarrollo',
'debug': True
}
## Fixture de alcance de clase - creada una vez por clase de prueba
@pytest.fixture(scope="class")
def base_datos_prueba():
# Setup
db = crear_base_datos_prueba()
yield db
# Teardown
db.limpiar()
El "teardown" se realiza automáticamente después de la prueba usando la palabra clave yield. Esto es especialmente útil para limpiar recursos después de las pruebas, como cerrar conexiones de bases de datos o archivos.
Parametrize: Probando Múltiples Escenarios
El decorador @pytest.mark.parametrize es extremadamente útil cuando necesitas ejecutar la misma prueba con diferentes valores de entrada. En lugar de escribir múltiples funciones de prueba, puedes usar parametrize para combinar todos los escenarios en una única función.
import pytest
@pytest.mark.parametrize("entrada,esperado", [
(2, 4), # 2 al cuadrado = 4
(3, 9), # 3 al cuadrado = 9
(5, 25), # 5 al cuadrado = 25
(10, 100), # 10 al cuadrado = 100
(0, 0), # 0 al cuadrado = 0
(-2, 4), # -2 al cuadrado = 4
])
def test_al_cuadrado(entrada, esperado):
assert entrada ** 2 == esperado
## Parametrize con múltiples parámetros
@pytest.mark.parametrize("a,b,operacion,resultado", [
(2, 3, 'sumar', 5),
(10, 5, 'restar', 5),
(4, 6, 'multiplicar', 24),
(20, 4, 'dividir', 5),
(3, 3, 'potencia', 27),
])
def test_calculadora(a, b, operacion, resultado):
if operacion == 'sumar':
assert a + b == resultado
elif operacion == 'restar':
assert a - b == resultado
elif operacion == 'multiplicar':
assert a * b == resultado
elif operacion == 'dividir':
assert a / b == resultado
elif operacion == 'potencia':
assert a ** b == resultado
También puedes usar IDs personalizados para identificar cada caso de prueba:
@pytest.mark.parametrize("numero,esperado", [
pytest.param(1, 1, id="uno"),
pytest.param(2, 2, id="dos"),
pytest.param(3, 6, id="tres_factorial"),
], ids=["uno", "dos", "tres"])
def test_ejemplos_con_ids(numero, esperado):
assert numero == esperado
Marcadores y Organización de Pruebas
Los marcadores (markers) permiten categorizar y filtrar pruebas según diferentes criterios. Esto es especialmente útil en proyectos grandes donde necesitas ejecutar solo un subconjunto de pruebas.
import pytest
## Marcadores personalizados
pytest.mark.lento
pytest.mark.rapido
pytest.mark.integracion
pytest.mark.unidad
pytest.mark.lento
pytest.mark.integracion
## Usando marcadores en las pruebas
def test_calculo_simple():
assert 1 + 1 == 2
@pytest.mark.integracion
def test_conexion_base_datos():
# Esta prueba requiere base de datos real
pass
@pytest.mark.lento
def test_procesamiento_masa():
# Esta prueba tarda mucho en ejecutarse
pass
## Marcando pruebas con skip y xfail
@pytest.mark.skip(reason="Funcionalidad aún no implementada")
def test_proxima_caracteristica():
pass
@pytest.mark.xfail(reason="Bug conocido, será corregido en la próxima versión")
def test_caracteristica_con_bug():
assert False
Para ejecutar solo pruebas de una categoría específica, usa el parámetro -m:
## Ejecutar solo pruebas de unidad
pytest -m unidad
## Ejecutar pruebas que no son lentas
pytest -m "not lento"
## Ejecutar pruebas de integración
pytest -m integracion
Pruebas de Excepciones
pytest facilita la verificación de excepciones usando pytest.raises(). Esta funcionalidad es esencial para probar código que debe lanzar errores en situaciones específicas.
import pytest
def validar_edad(edad):
if edad < 0:
raise ValueError("La edad no puede ser negativa")
if edad > 150:
raise ValueError("Edad inválida")
return True
def test_edad_valida():
assert validar_edad(25) == True
def test_edad_negativa():
with pytest.raises(ValueError) as exc_info:
validar_edad(-5)
assert "negativa" in str(exc_info.value)
def test_edad_muy_alta():
with pytest.raises(ValueError) as exc_info:
validar_edad(200)
assert "inválida" in str(exc_info.value)
## También podemos probar el tipo de excepción
def test_tipo_incorrecto():
with pytest.raises(TypeError):
validar_edad("veinticinco") # Pasando string en lugar de número
Mocking y Patch
Cuando necesitas probar código que depende de componentes externos (como APIs, bases de datos o sistemas de archivos), el mocking es esencial. pytest puede usarse junto con la biblioteca unittest.mock para crear mocks y stubs.
from unittest.mock import Mock, patch, MagicMock
import pytest
## Ejemplo: Probando una función que hace peticiones HTTP
def obtener_datos_api(url):
import requests
respuesta = requests.get(url)
return respuesta.json()
@patch('requests.get')
def test_obtener_datos_api_exito(mock_get):
# Configurar el mock
mock_respuesta = Mock()
mock_respuesta.json.return_value = {'nombre': 'Juan', 'edad': 30}
mock_get.return_value = mock_respuesta
# Ejecutar la prueba
resultado = obtener_datos_api('https://api.ejemplo.com/usuario')
# Verificar resultado
assert resultado == {'nombre': 'Juan', 'edad': 30}
mock_get.assert_called_once_with('https://api.ejemplo.com/usuario')
@patch('requests.get')
def test_obtener_datos_api_error(mock_get):
mock_get.side_effect = Exception("Error de conexión")
with pytest.raises(Exception) as exc_info:
obtener_datos_api('https://api.ejemplo.com/usuario')
assert "conexión" in str(exc_info.value)
## Usando MagicMock para objetos más complejos
@pytest.fixture
def mock_usuario():
usuario = MagicMock()
usuario.nombre = "María"
usuario.email = "[email protected]"
usuario.obtener_direccion.return_value = "Calle ABC, 123"
return usuario
def test_usuario_mock(mock_usuario):
assert mock_usuario.nombre == "María"
assert mock_usuario.obtener_direccion() == "Calle ABC, 123"
Cobertura de Código
Medir la cobertura de código es fundamental para garantizar que tus pruebas están realmente validando todas las líneas de tu código. pytest-cov ofrece esta funcionalidad.
## Ejecutar pruebas con coverage
pytest --cov=mi_modulo --cov-report=html
## Ver cobertura detallada por archivo
pytest --cov=mi_modulo --cov-report=term-missing
Para configurar el coverage en tu archivo pytest.ini o pyproject.toml:
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=html --cov-report=term"
Configuración Avanzada: pytest.ini y pyproject.toml
Puedes configurar pytest usando archivos de configuración para personalizar el comportamiento por defecto. Esto es especialmente útil en proyectos grandes.
## pytest.ini
[pytest]
testpaths = pruebas
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
markers =
lento: pruebas que tardan en ejecutarse
integracion: pruebas de integración
unidad: pruebas unitarias
O usando pyproject.toml (recomendado para proyectos modernos):
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
pythonpath = ["src"]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/venv/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
Mejores Prácticas con pytest
Para escribir pruebas efectivas y mantenibles, sigue estas mejores prácticas:
Nomenclatura clara: Nombra tus pruebas de forma descriptiva para que sea fácil entender qué verifica cada prueba sin necesidad de leer el código:
## Bueno
def test_calcular_promedio_debe_retornar_valor_correcto_cuando_lista_tiene_numeros():
pass
## Malo
def test_prom():
pass
Pruebas independientes: Cada prueba debe poder ejecutarse independientemente de las otras. Evita dependencias entre pruebas y limpia cualquier estado compartido:
@pytest.fixture(autouse=True)
def reiniciar_estado():
# Limpiar estado antes de cada prueba
EstadoGlobal.limpiar()
yield
# Limpiar después de la prueba también
EstadoGlobal.limpiar()
Una aserción por prueba (opcional): Aunque no es una regla estricta, muchos desarrolladores prefieren tener solo una aserción por prueba para facilitar la identificación del problema cuando una prueba falla.
Usa fixtures para datos reutilizables: Evita repetición de código creando fixtures para datos que se usan en varias pruebas.
Mantén las pruebas rápidas: Las pruebas lentas tienden a ejecutarse con menos frecuencia. Si una prueba es lenta, considera refactorizarla o dividirla en pruebas más pequeñas.
Documenta pruebas complejas: Cuando la lógica de la prueba no sea obvia, agrega docstrings explicando el escenario que se está probando.
Ejecutando Pruebas en Paralelo
Para proyectos grandes con muchas pruebas, ejecutar en paralelo puede reducir significativamente el tiempo de ejecución. El plugin pytest-xdist permite esto:
## Ejecutar con 4 workers
pytest -n 4
## Ejecutar con auto-detección de CPU
pytest -n auto
También puedes ejecutar pruebas específicas:
## Ejecutar un archivo específico
pytest tests/test_usuario.py
## Ejecutar una función específica
pytest tests/test_usuario.py::test_crear_usuario
## Ejecutar pruebas que coincidan con un patrón
pytest -k "test_usuario"
## Ejecutar pruebas con marcador específico
pytest -m "not lento"
Integración con CI/CD
pytest se integra perfectamente con pipelines de CI/CD como GitHub Actions, GitLab CI, Jenkins y otros. Aquí hay un ejemplo de configuración con GitHub Actions:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Conclusión
pytest es una herramienta esencial para cualquier desarrollador de Python que se toma en serio la calidad del código. Con su sintaxis simple, poderosa estructura de fixtures y vasto ecosistema de plugins, pytest hace que la escritura y mantenimiento de pruebas sea una tarea mucho más agradable.
Dominar pytest no es solo aprender la sintaxis, sino entender cómo escribir pruebas que realmente agregan valor a tu proyecto. Las pruebas bien escritas sirven como documentación ejecutable de tu código y protegen contra regresiones futuras.
Para continuar aprendiendo, explora la documentación oficial de pytest en https://docs.pytest.org y practica con proyectos reales. Cuantas más pruebas escribas, más natural se volverá el proceso de desarrollo orientado a pruebas.
Recuerda: las pruebas automatizadas son una inversión en el futuro de tu proyecto. El tiempo invertido hoy en escribir pruebas de calidad será compensado muchas veces a lo largo de la vida útil de tu código.
Enlaces Externos
- Documentación Oficial de pytest - Documentación completa y oficial del framework pytest
- Python.org - Guía de Pruebas - Documentación oficial de Python sobre pruebas
- Real Python - Tutorial de pytest - Tutorial completo de pytest
- DataCamp - Curso de pytest - Curso sobre desarrollo dirigido por pruebas
- Python Testing - Guía completa sobre pruebas en Python
- Toptal - Mejores Prácticas de pytest - Mejores prácticas con pytest
- JetBrains - Guía de pytest - Guía de pytest en PyCharm
- GitHub - pytest-dev - Repositorio oficial de pytest en GitHub
- Codecov - Cobertura de Python - Herramienta para cobertura de código Python