Saltar al contenido

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 pytest
2pip install pytest==8.2.2
3
4# Verificar
5pytest --version
6# pytest 8.2.2
7
8# 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.py
2
3def test_suma_basica():
4 """Verifica que 2 + 2 = 4. Obvio, pero demuestra la estructura."""
5 resultado = 2 + 2
6 assert resultado == 4
7
8def test_string_upper():
9 """Verifica que .upper() funciona."""
10 assert "hola".upper() == "HOLA"
11
12def 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 pytest
2
3def 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 error
7
8def 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 tests
2$ pytest test_limpieza.py -v
3
4test_limpieza.py::test_suma_basica PASSED
5test_limpieza.py::test_string_upper PASSED
6test_limpieza.py::test_lista_vacia PASSED
7
8====== 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.py
2import pandas as pd
3
4def 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)
10
11def 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.0
15 return df['importe'].mean()

Funciones de datos que vamos a testear

1# archivo: test_limpieza.py
2import pandas as pd
3from limpieza import limpiar_emails, calcular_ticket_medio
4
5def 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']
10
11def 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) == 2
16 assert resultado['email'].isnull().sum() == 0
17
18def test_ticket_medio_basico():
19 """Calcula correctamente la media."""
20 df = pd.DataFrame({'importe': [100, 200, 300]})
21 assert calcular_ticket_medio(df) == 200.0
22
23def 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 pd
3import pytest
4
5@pytest.fixture
6def 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 })
14
15@pytest.fixture
16def 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.py
2import pandas as pd
3
4def test_total_ventas(df_pedidos):
5 """La fixture df_pedidos se inyecta automáticamente."""
6 total = df_pedidos['importe'].sum()
7 assert total == 850.0
8
9def test_clientes_unicos(df_pedidos):
10 """Verifica el número de clientes únicos."""
11 unicos = df_pedidos['cliente'].nunique()
12 assert unicos == 3
13
14def 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

[01]

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.

Cargando editor...
[02]

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.

Cargando editor...
[03]

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).

Cargando editor...

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...