En este tema aprenderemos una distinción clave para practicar TDD: probar comportamiento no es lo mismo que probar implementación. Una buena prueba debería verificar qué hace el sistema desde afuera, no cómo está construido internamente.
Si una prueba depende demasiado de los detalles internos, cualquier refactor puede romperla aunque el comportamiento siga siendo correcto.
El comportamiento es lo que un usuario, otra función o un módulo cliente puede observar. En una función pura, suele ser el valor que devuelve para una entrada determinada.
Por ejemplo, si una función calcula el total de un carrito, el comportamiento observable es el total devuelto.
assert calcular_total(productos) == 75
La implementación es la forma interna en que el código logra ese comportamiento. Puede usar un ciclo for, una comprensión, sum, funciones auxiliares o clases.
Dos implementaciones distintas pueden producir exactamente el mismo resultado. Si las pruebas están bien orientadas al comportamiento, ambas deberían pasar.
Usaremos este requisito:
Este requisito habla del resultado, no de si debemos usar un ciclo, una lista intermedia o sum.
Esta prueba verifica el resultado observable:
Archivo a crear: tests/test_carrito.py
from tienda.carrito import calcular_total
def test_total_de_carrito_suma_precio_por_cantidad():
productos = [
{"nombre": "Libro", "precio": 30, "cantidad": 2},
{"nombre": "Lápiz", "precio": 5, "cantidad": 3},
]
total = calcular_total(productos)
assert total == 75
La prueba no dice cómo debe calcularse el total. Solo expresa qué resultado se espera.
Una implementación posible usa un ciclo:
Archivo a crear: 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
La prueba debería pasar.
Podemos refactorizar usando sum:
Archivo a modificar: src/tienda/carrito.py
def calcular_total(productos):
return sum(
producto["precio"] * producto["cantidad"]
for producto in productos
)
El comportamiento es el mismo. Por lo tanto, la prueba debería seguir pasando.
Una prueba mala intentaría verificar detalles internos. Por ejemplo, podría exigir que exista una función auxiliar concreta:
from tienda.carrito import calcular_subtotal_producto
def test_calcula_subtotal_de_producto():
producto = {"nombre": "Libro", "precio": 30, "cantidad": 2}
assert calcular_subtotal_producto(producto) == 60
Esta prueba puede ser útil si calcular_subtotal_producto es parte pública del diseño. Pero si solo era un detalle interno, la prueba hará más difícil refactorizar.
Una prueba probablemente está demasiado atada a la implementación si:
En este ejemplo, la función pública es calcular_total. El resto puede cambiar si lo necesitamos.
Una prueba enfocada en la API pública tiene más valor a largo plazo:
assert calcular_total(productos) == 75
Mientras esa afirmación siga siendo verdadera, podemos mejorar la implementación interna.
No significa que nunca podamos probar funciones auxiliares. Si una función auxiliar representa una regla de negocio importante y estable, puede merecer pruebas propias.
La pregunta es: ¿esta función es un comportamiento que queremos sostener o solo una decisión interna actual?
Supongamos este código:
LONGITUD_MINIMA_PASSWORD = 8
def tiene_longitud_minima(contrasena):
return len(contrasena) >= LONGITUD_MINIMA_PASSWORD
def es_password_valido(contrasena):
return tiene_longitud_minima(contrasena)
Si tiene_longitud_minima solo existe para organizar el código, puede ser suficiente probar es_password_valido.
La prueba pública puede ser:
from seguridad.password import es_password_valido
def test_password_es_valido_si_tiene_ocho_caracteres():
assert es_password_valido("abcdefgh") is True
Esta prueba seguirá pasando aunque más adelante eliminemos, renombremos o reorganicemos tiene_longitud_minima.
Esta prueba ata la suite a una función auxiliar:
from seguridad.password import tiene_longitud_minima
def test_tiene_longitud_minima():
assert tiene_longitud_minima("abcdefgh") is True
Puede ser correcta si decidimos que esa función es pública. Si no, reduce libertad para refactorizar.
Podemos eliminar la función auxiliar:
Archivo a modificar: src/seguridad/password.py
LONGITUD_MINIMA_PASSWORD = 8
def es_password_valido(contrasena):
return len(contrasena) >= LONGITUD_MINIMA_PASSWORD
Las pruebas de es_password_valido deberían seguir pasando. Las pruebas de tiene_longitud_minima fallarían aunque el comportamiento público siga igual.
Antes de escribir una prueba, pregúntate:
Hay casos donde detalles técnicos importan: rendimiento, orden de llamadas a una dependencia externa, uso de una transacción o comunicación con una API. Pero esos casos deben tener una razón concreta.
En este curso todavía estamos priorizando lógica de dominio y funciones puras, por eso conviene favorecer pruebas orientadas al comportamiento.
Revisa una prueba de los temas anteriores y responde:
Luego ejecuta python -m pytest para comprobar que la suite sigue en verde.
Antes de continuar, verifica lo siguiente:
python -m pytest después de cambiar pruebas o código.En este tema vimos que las pruebas más valiosas en TDD suelen enfocarse en comportamiento observable. Eso nos permite mejorar la implementación interna sin romper pruebas que no deberían romperse.
En el próximo tema aplicaremos estas ideas al diseño incremental de funciones puras, donde cada ejemplo empuja el código hacia una solución más clara.