lección 3
Instalar pytest: testing automatizado
pytest, el framework de testing de Python: escribe los tests automáticos que garantizan la calidad de tus datos y pipelines en cada cambio.
⏱ 50 min
### ¿Cómo sabes que tu pipeline no está roto?
En las dos lecciones anteriores viste POR QUÉ la calidad de datos es tu trabajo más importante y aprendiste a clasificar los problemas en cinco dimensiones. Ahora te falta la herramienta para AUTOMATIZAR esas comprobaciones: pytest, el framework con el que escribirás los tests que garantizan la calidad de tus datos y de tus pipelines. Todo lo que a partir de la próxima lección validarás (completitud, unicidad, rangos, frescura) acabará viviendo dentro de tests de pytest que se ejecutan solos en cada cambio. Por eso lo instalamos y lo dominamos ahora: es el esqueleto sobre el que montarás toda tu estrategia de calidad.
Es viernes a las 5 de la tarde. Tu colega hizo un "pequeño cambio" en la función de limpieza de datos. El lunes por la mañana, el dashboard del CEO muestra números imposibles. El director de ventas llama furioso. Nadie sabe qué cambió, cuándo ni por qué. Este escenario se repite CADA SEMANA en equipos sin tests.
Los tests automatizados son tu seguro de vida. Son código que verifica que tu código funciona como esperas. Cada vez que cambias algo, ejecutas los tests: si pasan, puedes desplegar con confianza. Si fallan, sabes EXACTAMENTE qué se rompió antes de que llegue a producción. Es la diferencia entre "creo que funciona" y "SÉ que funciona".
Para datos, los tests son aún más críticos. Un bug en una app web muestra un error al usuario. Un bug en un pipeline de datos produce datos incorrectos que parecen correctos: nadie nota el error hasta que alguien toma una decisión de negocio basada en datos falsos. Los tests detectan estos errores silenciosos.
### Paso 1: Instalar pytest
1# Instalar pytest2pip install pytest==8.2.234# Verificar5pytest --version6# pytest 8.2.278# Ejecutar tests (busca archivos test_*.py automáticamente)9pytest
pytest: el framework de testing más popular de Python
pytest es el estándar de facto en Python. Es más moderno y expresivo que unittest (la librería estándar). Su magia: un test es simplemente una función que empieza por test_ y usa assert para verificar condiciones. Sin clases, sin decoradores raros, sin boilerplate. Simple y potente.
### Tu primer test: la anatomía mínima
1# archivo: test_limpieza.py23def test_suma_basica():4 """Verifica que 2 + 2 = 4. Obvio, pero demuestra la estructura."""5 resultado = 2 + 26 assert resultado == 478def test_string_upper():9 """Verifica que .upper() funciona."""10 assert "hola".upper() == "HOLA"1112def test_lista_vacia():13 """Verifica que una lista vacía es falsy."""14 assert not []
Un test = una función test_* con asserts. Así de simple.
Y cuando lo correcto es que la función REVIENTE — porque le pasas datos inválidos —, se usa pytest.raises. El test pasa si la excepción se lanza, y falla si la función devuelve algo sin reventar:
1import pytest23def test_division_por_cero():4 """Verifica que dividir por cero lanza ZeroDivisionError."""5 with pytest.raises(ZeroDivisionError):6 1 / 0 # <- el test pasa PORQUE salta el error78def test_valor_negativo_lanza_error():9 """Verifica que nuestra función rechaza entradas inválidas."""10 with pytest.raises(ValueError):11 calcular_descuento(-10, 20)
pytest.raises: el test pasa si la excepción se lanza. Si la función no revienta, el test FALLA.
1# Ejecutar los tests2$ pytest test_limpieza.py -v34test_limpieza.py::test_suma_basica PASSED5test_limpieza.py::test_string_upper PASSED6test_limpieza.py::test_lista_vacia PASSED78====== 3 passed in 0.02s ======
pytest -v muestra cada test y su resultado
### Tests para funciones de datos
En ingeniería de datos, no testeas sumas triviales: testeas que tu función de limpieza maneja nulos correctamente, que el parsing de fechas no falla con formatos mixtos, que los duplicados se eliminan bien. Aquí es donde los tests salvan vidas.
1# archivo: limpieza.py2import pandas as pd34def limpiar_emails(df: pd.DataFrame) -> pd.DataFrame:5 """Normaliza emails: strip, lower, elimina nulos."""6 df = df.copy()7 df['email'] = df['email'].str.strip().str.lower()8 df = df.dropna(subset=['email'])9 return df.reset_index(drop=True)1011def calcular_ticket_medio(df: pd.DataFrame) -> float:12 """Calcula el ticket medio de un DataFrame de pedidos."""13 if df.empty or 'importe' not in df.columns:14 return 0.015 return df['importe'].mean()
Funciones de datos que vamos a testear
1# archivo: test_limpieza.py2import pandas as pd3from limpieza import limpiar_emails, calcular_ticket_medio45def test_limpiar_emails_normaliza():6 """Los emails se normalizan a minúsculas sin espacios."""7 df = pd.DataFrame({'email': [' Ana@Mail.COM ', ' CARLOS@gmail.com ']})8 resultado = limpiar_emails(df)9 assert resultado['email'].tolist() == ['ana@mail.com', 'carlos@gmail.com']1011def test_limpiar_emails_elimina_nulos():12 """Las filas con email nulo se eliminan."""13 df = pd.DataFrame({'email': ['ana@mail.com', None, 'carlos@mail.com']})14 resultado = limpiar_emails(df)15 assert len(resultado) == 216 assert resultado['email'].isnull().sum() == 01718def test_ticket_medio_basico():19 """Calcula correctamente la media."""20 df = pd.DataFrame({'importe': [100, 200, 300]})21 assert calcular_ticket_medio(df) == 200.02223def test_ticket_medio_dataframe_vacio():24 """DataFrame vacío retorna 0."""25 df = pd.DataFrame()26 assert calcular_ticket_medio(df) == 0.0
Tests para funciones de datos: prueban el happy path Y los edge cases
Consejo de senior: escribe tests para los EDGE CASES, no para los casos obvios. ¿Qué pasa si el DataFrame está vacío? ¿Y si todos los valores son nulos? ¿Y si una columna no existe? Esos son los bugs que llegarán a producción un sábado a las 3AM.
### Estructura de un proyecto con tests
- mi_pipeline/ — carpeta raíz del proyecto
- mi_pipeline/src/ — código de producción (limpieza.py, ingesta.py, etc.)
- mi_pipeline/tests/ — tests (test_limpieza.py, test_ingesta.py, etc.)
- mi_pipeline/tests/conftest.py — fixtures compartidas entre tests
- mi_pipeline/pytest.ini — configuración de pytest
### Fixtures: datos de prueba reutilizables
Si varios tests necesitan el mismo DataFrame de ejemplo, no lo repitas en cada test. Usa fixtures: funciones que preparan datos y los "inyectan" en tus tests automáticamente.
1# archivo: conftest.py (pytest lo detecta automáticamente)2import pandas as pd3import pytest45@pytest.fixture6def df_pedidos():7 """DataFrame de pedidos para tests."""8 return pd.DataFrame({9 'pedido_id': [1, 2, 3, 4, 5],10 'cliente': ['Ana', 'Carlos', 'Ana', 'Pedro', 'Carlos'],11 'importe': [150.0, 200.0, 75.0, 300.0, 125.0],12 'fecha': pd.to_datetime(['2024-01-15', '2024-01-16', '2024-01-17', '2024-01-18', '2024-01-19'])13 })1415@pytest.fixture16def df_pedidos_con_nulos():17 """DataFrame con datos sucios para probar limpieza."""18 return pd.DataFrame({19 'pedido_id': [1, 2, 3, None, 5],20 'cliente': ['Ana', None, 'Lucía', 'Pedro', 'María'],21 'importe': [150.0, None, 75.0, 300.0, -50.0],22 'fecha': ['2024-01-15', 'fecha_invalida', '2024-01-17', None, '2024-01-19']23 })
Fixtures en conftest.py: datos de prueba que se reutilizan en todos los tests
1# archivo: test_pipeline.py2import pandas as pd34def test_total_ventas(df_pedidos):5 """La fixture df_pedidos se inyecta automáticamente."""6 total = df_pedidos['importe'].sum()7 assert total == 850.089def test_clientes_unicos(df_pedidos):10 """Verifica el número de clientes únicos."""11 unicos = df_pedidos['cliente'].nunique()12 assert unicos == 31314def test_limpieza_nulos(df_pedidos_con_nulos):15 """Verifica que la limpieza maneja el DataFrame sucio."""16 assert df_pedidos_con_nulos['importe'].isnull().sum() == 1
Los tests reciben fixtures como parámetros: pytest las inyecta automáticamente
El patrón AAA (Arrange-Act-Assert) es tu mejor amigo: 1) Prepara los datos (Arrange), 2) Ejecuta la función (Act), 3) Verifica el resultado (Assert). Cada test debe tener exactamente UNA razón para fallar.
No testees contra datos de producción ni contra APIs externas en tus tests unitarios. Los tests deben ser rápidos (<5 segundos todos), deterministas (siempre el mismo resultado) y sin dependencias externas. Usa fixtures con datos falsos controlados.
Con pytest en tu cinturón, ya tienes el motor que ejecuta las comprobaciones. En la próxima lección lo pondrás a trabajar: convertirás las cinco dimensiones de calidad en validadores de datos con Python puro, y esos validadores acabarán envueltos en tests de pytest. Cada regla de negocio que descubras se convierte en un test que la protege para siempre. Más adelante daremos el salto a Great Expectations, pero la mentalidad (escribir una comprobación por cada cosa que puede salir mal) nace aquí.
## ejercicios
Tests para función de limpieza de ciudades
Escribe 4 tests para una función normalizar_ciudad(texto) que: quita espacios, pone la primera letra en mayúscula, y devuelve "Desconocida" si recibe None o string vacío. Este ejercicio son DOS ficheros. Créalos en la misma carpeta: utils.py ← la función (cópiala tal cual, no hay que cambiarla) test_utils.py ← tus tests Luego, desde esa carpeta: pytest -v Si ves "No module named utils", es que están en carpetas distintas.
Crear fixture y tests para validación de datos
Crea una fixture df_ventas con datos de ejemplo y escribe 3 tests que validen: 1) No hay importes negativos, 2) Todas las fechas son del año 2024, 3) No hay cliente_id nulo.
Tests de edge cases para calcular_descuento
Tienes una función calcular_descuento(precio, porcentaje) que aplica un descuento. Escribe tests para: precio 0, porcentaje 0, porcentaje 100, porcentaje negativo (debe lanzar ValueError), y precio negativo (debe lanzar ValueError).
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...