15. TDD con colecciones: listas, diccionarios y transformaciones de datos

15.1 Objetivo del tema

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.

Objetivo práctico: diseñar transformaciones de datos con pruebas pequeñas, evitando mezclar varias reglas en una sola implementación inicial.

15.2 Datos del ejemplo

Representaremos cada pedido con un diccionario:

pedido = {
    "cliente": "Ana",
    "total": 120,
    "estado": "pagado",
}

Una lista de pedidos contendrá varios diccionarios con la misma estructura.

15.3 Primera función: filtrar pedidos pagados

El primer requisito será:

Dada una lista de pedidos, devolver solo los pedidos con estado "pagado".

Empezamos con un caso mínimo.

15.4 Primera prueba con una lista pequeña

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.

15.5 Implementación mínima

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.

15.6 Segundo ejemplo: excluir pendientes

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.

15.7 Implementar el filtrado

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

15.8 Refactor con comprensión de listas

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.

15.9 Caso borde: lista vacía

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.

15.10 Segunda función: obtener nombres de clientes

Nuevo requisito:

Dada una lista de pedidos, devolver una lista con los nombres de los clientes.

Esto es una transformación: pasamos de lista de diccionarios a lista de cadenas.

15.11 Prueba para transformar datos

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.

15.12 Implementar transformació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.

15.13 Tercera función: sumar totales

Nuevo requisito:

Dada una lista de pedidos, calcular la suma de sus totales.

Primero escribimos una prueba.

15.14 Prueba para sumar totales

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.

15.15 Implementar suma de 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.

15.16 Combinar funciones existentes

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.

15.17 Implementar composición

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.

15.18 Cuidado con mutar la entrada

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.

15.19 Errores frecuentes

  • Probar demasiadas transformaciones juntas: dificulta saber qué regla falló.
  • Modificar la lista original sin querer: puede generar errores difíciles de rastrear.
  • No probar listas vacías: suelen revelar supuestos incorrectos.
  • Mezclar filtrado, transformación y suma en la primera función: complica el diseño incremental.
  • Usar datos enormes en pruebas unitarias: para TDD suelen alcanzar ejemplos pequeños y expresivos.

15.20 Ejercicio propuesto

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.

  • Escribe primero una prueba con dos pedidos.
  • Ejecuta python -m pytest y verifica que falle.
  • Implementa lo mínimo.
  • Agrega una prueba para lista vacía.
  • Refactoriza si aparece duplicación.

15.21 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • Probaste filtrado de listas con ejemplos pequeños.
  • Probaste transformación de diccionarios a otros valores.
  • Probaste suma de datos numéricos dentro de una colección.
  • Incluiste al menos un caso de lista vacía.
  • Verificaste que una función no muta la lista original cuando eso importa.
  • Ejecutaste python -m pytest después de cada cambio.

15.22 Conclusión

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.