10. Patrón Arrange, Act, Assert en pruebas de TDD

10.1 Objetivo del tema

En este tema aprenderemos a ordenar el cuerpo de una prueba usando el patrón Arrange, Act, Assert. Este patrón ayuda a que la prueba sea fácil de leer, fácil de modificar y útil como documentación del comportamiento.

En TDD, una prueba clara es importante porque guía el diseño. Si la prueba está desordenada, también será más difícil entender qué comportamiento estamos pidiendo.

Objetivo práctico: escribir pruebas con una estructura clara de preparación, acción y verificación usando Python y pytest.

10.2 Qué significa Arrange, Act, Assert

El patrón divide la prueba en tres partes:

  • Arrange: preparar datos, objetos y condiciones iniciales.
  • Act: ejecutar la acción que queremos probar.
  • Assert: verificar el resultado esperado.

La prueba queda organizada como una pequeña historia: dado un escenario, cuando ocurre una acción, entonces esperamos un resultado.

10.3 Ejemplo simple

Supongamos que probamos una función que aplica descuentos:

def test_aplicar_descuento_del_diez_por_ciento():
    precio = 100
    descuento = 10

    resultado = aplicar_descuento(precio, descuento)

    assert resultado == 90

La primera parte prepara datos, la segunda ejecuta la función y la tercera verifica el resultado.

10.4 Separar visualmente las partes

Una forma simple de hacer visible el patrón es separar las secciones con líneas en blanco:

def test_aplicar_descuento_del_diez_por_ciento():
    precio = 100
    descuento = 10

    resultado = aplicar_descuento(precio, descuento)

    assert resultado == 90

No hace falta agregar comentarios si la prueba es clara. Las líneas en blanco ya muestran la estructura.

10.5 Cuándo usar comentarios AAA

En pruebas más largas, puede ser útil agregar comentarios:

def test_aplicar_descuento_del_diez_por_ciento():
    # Arrange
    precio = 100
    descuento = 10

    # Act
    resultado = aplicar_descuento(precio, descuento)

    # Assert
    assert resultado == 90

En pruebas pequeñas, los comentarios pueden ser innecesarios. Lo importante es que el lector pueda reconocer las tres partes.

10.6 Ejemplo desordenado

Esta prueba funciona, pero mezcla datos, acción y verificación en una sola línea:

def test_envio_es_gratis_si_total_es_cien_o_mas():
    assert calcular_envio(100) == 0

Para una prueba tan simple puede estar bien. Pero si el escenario crece, conviene separar las partes.

10.7 Ejemplo ordenado

La misma prueba puede expresarse con AAA:

def test_envio_es_gratis_si_total_es_cien_o_mas():
    total = 100

    resultado = calcular_envio(total)

    assert resultado == 0

Ahora se ve claramente el dato de entrada, la acción y el resultado esperado.

10.8 Caso práctico: carrito de compras

Vamos a trabajar con una función que calcula el total de un carrito. Cada producto se representa como un diccionario con nombre, precio y cantidad.

Requisito: el total del carrito es la suma de precio por cantidad de cada producto.

10.9 Primera prueba con AAA

Escribimos una prueba para un carrito con un solo producto.

Archivo a crear: tests/test_carrito.py

from tienda.carrito import calcular_total


def test_total_de_carrito_con_un_producto():
    productos = [
        {"nombre": "Libro", "precio": 30, "cantidad": 2},
    ]

    total = calcular_total(productos)

    assert total == 60

Ejecutamos:

python -m pytest

La prueba debe fallar porque todavía no implementamos la función.

10.10 Implementación mínima

Escribimos el código mínimo para pasar esta prueba.

Archivo a crear: src/tienda/carrito.py

def calcular_total(productos):
    producto = productos[0]
    return producto["precio"] * producto["cantidad"]

Ejecutamos python -m pytest. La prueba debería pasar.

10.11 Segunda prueba: varios productos

Ahora agregamos una prueba que obliga a sumar más de un producto.

Archivo a modificar: tests/test_carrito.py

from tienda.carrito import calcular_total


def test_total_de_carrito_con_un_producto():
    productos = [
        {"nombre": "Libro", "precio": 30, "cantidad": 2},
    ]

    total = calcular_total(productos)

    assert total == 60


def test_total_de_carrito_con_varios_productos():
    productos = [
        {"nombre": "Libro", "precio": 30, "cantidad": 2},
        {"nombre": "Lápiz", "precio": 5, "cantidad": 3},
    ]

    total = calcular_total(productos)

    assert total == 75

La estructura AAA se mantiene en cada prueba.

10.12 Generalizar la implementación

La segunda prueba nos obliga a recorrer todos los productos.

Archivo a modificar: src/tienda/carrito.py

def calcular_total(productos):
    total = 0
    for producto in productos:
        total += producto["precio"] * producto["cantidad"]
    return total

Ejecutamos:

python -m pytest

Si ambas pruebas pasan, estamos en verde.

10.13 Refactor con sum

Con la suite en verde, podemos simplificar la implementación usando sum:

Archivo a modificar: src/tienda/carrito.py

def calcular_total(productos):
    return sum(
        producto["precio"] * producto["cantidad"]
        for producto in productos
    )

Ejecutamos python -m pytest. Si todo pasa, el refactor mantuvo el comportamiento.

10.14 Act debe ser una acción clara

En una prueba AAA, la parte Act debería ser fácil de identificar. Normalmente es una línea que llama a la función o método probado.

total = calcular_total(productos)

Si una prueba tiene muchas acciones principales, quizá está probando demasiadas cosas a la vez.

10.15 Assert debe verificar el comportamiento

La parte Assert debe verificar el resultado observable:

assert total == 75

Evita verificar detalles internos si no forman parte del comportamiento esperado. La prueba debe seguir siendo válida aunque cambie la implementación interna.

10.16 Arrange no debe ocultar demasiado

Preparar datos es necesario, pero si el Arrange crece demasiado, la prueba se vuelve difícil de leer. En ese caso podemos extraer funciones auxiliares con nombres claros.

def producto(nombre, precio, cantidad):
    return {"nombre": nombre, "precio": precio, "cantidad": cantidad}

La función auxiliar debe hacer la prueba más clara, no esconder información importante.

10.17 Prueba con helper de datos

Aplicando el helper, la prueba puede quedar así:

from tienda.carrito import calcular_total


def producto(nombre, precio, cantidad):
    return {"nombre": nombre, "precio": precio, "cantidad": cantidad}


def test_total_de_carrito_con_varios_productos():
    productos = [
        producto("Libro", 30, 2),
        producto("Lápiz", 5, 3),
    ]

    total = calcular_total(productos)

    assert total == 75

El Arrange sigue siendo visible, pero hay menos ruido sintáctico.

10.18 Errores frecuentes

  • Mezclar acción y verificación: dificulta entender qué se ejecuta y qué se espera.
  • Hacer demasiadas acciones en una prueba: puede indicar que la prueba cubre varios comportamientos.
  • Preparar datos innecesarios: vuelve la prueba más larga y menos clara.
  • Usar helpers opacos: si el helper oculta la regla, la prueba pierde valor documental.
  • Agregar asserts sin relación: una prueba debería tener un foco claro.

10.19 Ejercicio propuesto

Escribe una prueba AAA para este requisito:

Un carrito vacío debe tener total 0.

Primero escribe la prueba, ejecútala con python -m pytest, implementa lo mínimo si falla y refactoriza solo si el código queda más claro.

10.20 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • La prueba separa preparación, acción y verificación.
  • La acción principal se identifica rápidamente.
  • El assert verifica comportamiento observable.
  • El Arrange contiene solo datos necesarios.
  • Los helpers de prueba tienen nombres claros.
  • Ejecutaste python -m pytest después de cada cambio.

10.21 Conclusión

En este tema organizamos pruebas con Arrange, Act, Assert. Esta estructura hace que cada prueba sea más fácil de leer y ayuda a mantener claro qué escenario preparamos, qué acción ejecutamos y qué resultado esperamos.

En el próximo tema veremos la diferencia entre probar comportamiento y probar implementación, una distinción clave para evitar pruebas frágiles en TDD.