En este tema aplicaremos TDD a funciones que trabajan con colecciones de datos. Usaremos listas y diccionarios, estructuras muy comunes en programas Python.
Construiremos funciones para procesar pedidos de una tienda. Empezaremos con casos simples y agregaremos reglas paso a paso.
Representaremos cada pedido con un diccionario:
pedido = {
"cliente": "Ana",
"total": 120,
"estado": "pagado",
}
Una lista de pedidos contendrá varios diccionarios con la misma estructura.
El primer requisito será:
Empezamos con un caso mínimo.
Archivo a crear: tests/test_pedidos.py
from tienda.pedidos import filtrar_pagados
def test_filtrar_pagados_devuelve_pedido_pagado():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
]
resultado = filtrar_pagados(pedidos)
assert resultado == [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
]
Ejecutamos:
python -m pytest
La prueba debe fallar porque la función todavía no existe.
Creamos la función con lo mínimo para pasar la prueba.
Archivo a crear: src/tienda/pedidos.py
def filtrar_pagados(pedidos):
return pedidos
Ejecutamos python -m pytest. La prueba debería pasar, aunque la función todavía no filtra realmente.
Agregamos un pedido pendiente para forzar el filtrado.
Archivo a modificar: tests/test_pedidos.py
from tienda.pedidos import filtrar_pagados
def test_filtrar_pagados_excluye_pedidos_pendientes():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
{"cliente": "Luis", "total": 80, "estado": "pendiente"},
]
resultado = filtrar_pagados(pedidos)
assert resultado == [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
]
Ejecutamos python -m pytest. Ahora la prueba debe fallar.
La prueba nueva justifica recorrer la lista y seleccionar elementos.
Archivo a modificar: src/tienda/pedidos.py
def filtrar_pagados(pedidos):
pagados = []
for pedido in pedidos:
if pedido["estado"] == "pagado":
pagados.append(pedido)
return pagados
Ejecutamos:
python -m pytest
Con la suite en verde, podemos simplificar:
Archivo a modificar: src/tienda/pedidos.py
def filtrar_pagados(pedidos):
return [
pedido
for pedido in pedidos
if pedido["estado"] == "pagado"
]
Ejecutamos python -m pytest. El comportamiento debe mantenerse.
Una lista vacía debería devolver una lista vacía.
Archivo a modificar: tests/test_pedidos.py
def test_filtrar_pagados_devuelve_lista_vacia_si_no_hay_pedidos():
assert filtrar_pagados([]) == []
Esta prueba probablemente ya pase, pero documenta el comportamiento.
Nuevo requisito:
Esto es una transformación: pasamos de lista de diccionarios a lista de cadenas.
Archivo a modificar: tests/test_pedidos.py
from tienda.pedidos import filtrar_pagados, obtener_clientes
def test_obtener_clientes_devuelve_nombres_de_pedidos():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
{"cliente": "Luis", "total": 80, "estado": "pendiente"},
]
resultado = obtener_clientes(pedidos)
assert resultado == ["Ana", "Luis"]
Ejecutamos python -m pytest. La prueba debe fallar porque falta la función.
Archivo a modificar: src/tienda/pedidos.py
def filtrar_pagados(pedidos):
return [
pedido
for pedido in pedidos
if pedido["estado"] == "pagado"
]
def obtener_clientes(pedidos):
return [
pedido["cliente"]
for pedido in pedidos
]
Ejecutamos python -m pytest para confirmar el verde.
Nuevo requisito:
Primero escribimos una prueba.
Archivo a modificar: tests/test_pedidos.py
from tienda.pedidos import filtrar_pagados, obtener_clientes, sumar_totales
def test_sumar_totales_devuelve_suma_de_importes():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
{"cliente": "Luis", "total": 80, "estado": "pendiente"},
]
resultado = sumar_totales(pedidos)
assert resultado == 200
Ejecutamos python -m pytest. La prueba debe fallar porque falta sumar_totales.
Archivo a modificar: src/tienda/pedidos.py
def sumar_totales(pedidos):
return sum(pedido["total"] for pedido in pedidos)
Agrega esta función al módulo y ejecuta python -m pytest.
Podemos calcular el total de pedidos pagados combinando funciones:
Archivo a modificar: tests/test_pedidos.py
from tienda.pedidos import total_pagado
def test_total_pagado_suma_solo_pedidos_pagados():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
{"cliente": "Luis", "total": 80, "estado": "pendiente"},
{"cliente": "Eva", "total": 50, "estado": "pagado"},
]
assert total_pagado(pedidos) == 170
Esta prueba pide una nueva función de más alto nivel.
Podemos reutilizar funciones ya probadas:
Archivo a modificar: src/tienda/pedidos.py
def total_pagado(pedidos):
return sumar_totales(filtrar_pagados(pedidos))
Ejecutamos python -m pytest. Si todo pasa, la composición funciona.
Las funciones de transformación suelen ser más fáciles de probar si no modifican la lista original.
Podemos agregar una prueba para proteger ese comportamiento:
def test_filtrar_pagados_no_modifica_lista_original():
pedidos = [
{"cliente": "Ana", "total": 120, "estado": "pagado"},
{"cliente": "Luis", "total": 80, "estado": "pendiente"},
]
filtrar_pagados(pedidos)
assert len(pedidos) == 2
Esta prueba verifica que filtrar no elimina elementos de la lista original.
Agrega una función pedidos_mayores_a que reciba una lista de pedidos y un importe mínimo. Debe devolver solo los pedidos cuyo total sea mayor que ese importe.
python -m pytest y verifica que falle.Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio.En este tema usamos TDD con listas, diccionarios y transformaciones de datos. Dividimos el problema en operaciones pequeñas: filtrar, transformar, sumar y componer funciones.
En el próximo tema aplicaremos TDD con clases pequeñas, estado, invariantes y métodos públicos.