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.
El patrón divide la prueba en tres partes:
La prueba queda organizada como una pequeña historia: dado un escenario, cuando ocurre una acción, entonces esperamos un resultado.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Escribe una prueba AAA para este requisito:
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.
Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio.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.