En este tema aprenderemos a reconocer pruebas frágiles mientras aplicamos TDD. Una prueba frágil falla con facilidad por razones que no representan un defecto real en el comportamiento del sistema.
El objetivo práctico es distinguir entre una prueba que protege una regla importante y una prueba que solo está atada a detalles accidentales de implementación.
Una prueba frágil es una prueba que se rompe ante cambios internos razonables, aunque el comportamiento visible siga siendo correcto.
No toda prueba que falla es frágil. Si falla porque rompimos una regla del negocio, está cumpliendo su función. El problema aparece cuando falla por motivos irrelevantes.
Una señal frecuente de fragilidad es verificar atributos, estructuras o métodos privados que no forman parte del contrato público del objeto.
Ejemplo a evitar:
def test_deposito_guarda_el_saldo_en_un_atributo_interno():
cuenta = CuentaBancaria()
cuenta.depositar(100)
assert cuenta._saldo == 100
Esta prueba obliga a que el saldo se guarde exactamente en _saldo. Si mañana
el saldo se calcula desde movimientos, la prueba fallará aunque la cuenta siga funcionando.
La prueba debería comprobar lo que el sistema promete hacia afuera. Si el saldo es parte del comportamiento público, podemos verificarlo sin depender del atributo interno usado para almacenarlo.
Archivo a crear o modificar: tests/test_cuenta.py
def test_deposito_aumenta_el_saldo_disponible():
cuenta = CuentaBancaria()
cuenta.depositar(100)
assert cuenta.saldo == 100
Esta prueba permite refactorizar la implementación mientras se mantenga el resultado esperado por el dominio.
Algunas pruebas fallan porque esperan un orden que el requisito no exige.
Supongamos una función que devuelve etiquetas de una compra:
Archivo a crear o modificar: tests/test_etiquetas.py
def test_etiquetas_de_compra():
etiquetas = obtener_etiquetas_compra(total=120, cliente_frecuente=True)
assert etiquetas == ["cliente_frecuente", "compra_grande"]
Si el requisito solo dice que ambas etiquetas deben estar presentes, el orden exacto no debería importar.
Cuando el orden no es parte del comportamiento, la prueba debe expresarlo.
Archivo a crear o modificar: tests/test_etiquetas.py
def test_etiquetas_de_compra():
etiquetas = obtener_etiquetas_compra(total=120, cliente_frecuente=True)
assert set(etiquetas) == {"cliente_frecuente", "compra_grande"}
Ahora la prueba protege la regla correcta: deben aparecer las dos etiquetas, sin imponer una secuencia innecesaria.
Una prueba puede volverse frágil cuando compara una salida completa aunque solo una parte sea relevante para la regla.
Ejemplo a evitar:
def test_resumen_de_cuenta():
cuenta = CuentaBancaria()
cuenta.depositar(100)
resumen = generar_resumen(cuenta)
assert resumen == {
"titulo": "Resumen de cuenta",
"saldo": 100,
"cantidad_movimientos": 1,
"mensaje": "Gracias por operar con nosotros"
}
Si la prueba busca validar el saldo, está comprobando demasiadas cosas al mismo tiempo.
Un cambio de texto en mensaje podría romperla sin afectar la regla que quería
proteger.
En TDD conviene que cada prueba tenga una razón clara para fallar. Si el comportamiento importante es el saldo del resumen, la prueba debe enfocarse en eso.
def test_resumen_incluye_el_saldo_actual():
cuenta = CuentaBancaria()
cuenta.depositar(100)
resumen = generar_resumen(cuenta)
assert resumen["saldo"] == 100
Esta versión es menos frágil porque no mezcla varias expectativas independientes en una sola comparación.
Las pruebas que usan directamente la fecha u hora del sistema pueden fallar según el día, la zona horaria o el momento exacto de ejecución.
Ejemplo frágil:
from datetime import date
def test_promocion_de_fin_de_mes():
descuento = calcular_descuento_por_fecha(date.today())
assert descuento == 10
Esta prueba no siempre tiene el mismo resultado. Depende del día en que se ejecute.
La solución más simple es pasar explícitamente la fecha que queremos probar.
Archivo a crear o modificar: tests/test_promociones.py
from datetime import date
def test_promocion_de_fin_de_mes():
fecha = date(2026, 5, 31)
descuento = calcular_descuento_por_fecha(fecha)
assert descuento == 10
Ahora la prueba es determinística: debería dar el mismo resultado hoy, mañana y dentro de varios meses.
Una prueba es frágil cuando su resultado depende de lo que otra prueba ejecutó antes. Esto suele pasar con listas globales, objetos compartidos o fixtures con scope demasiado amplio.
Ejemplo a evitar:
cuenta = CuentaBancaria()
def test_deposito():
cuenta.depositar(100)
assert cuenta.saldo == 100
def test_retiro():
cuenta.retirar(40)
assert cuenta.saldo == 60
Si se ejecuta solo test_retiro, la prueba puede fallar porque depende del
depósito hecho por otra prueba.
Cada prueba debe preparar su propio escenario. Las fixtures pueden ayudar, siempre que entreguen objetos nuevos y no compartan estado mutable.
import pytest
@pytest.fixture
def cuenta_con_saldo():
cuenta = CuentaBancaria()
cuenta.depositar(100)
return cuenta
def test_deposito_aumenta_saldo():
cuenta = CuentaBancaria()
cuenta.depositar(100)
assert cuenta.saldo == 100
def test_retiro_disminuye_saldo(cuenta_con_saldo):
cuenta_con_saldo.retirar(40)
assert cuenta_con_saldo.saldo == 60
Ahora cada prueba tiene el estado que necesita y puede ejecutarse sola o junto con toda la suite.
Una prueba frágil puede describir cómo debe resolverse el problema, en lugar de describir qué resultado espera el usuario o el dominio.
Ejemplo a evitar:
def test_calculo_de_puntos_usa_multiplicador_interno():
puntos = calcular_puntos(total=100, nivel="oro")
assert puntos == 100 * 2 + 10
Si el negocio solo exige que un cliente oro reciba 210 puntos por una compra de 100, la prueba no necesita exponer la fórmula interna.
Es mejor nombrar el comportamiento esperado y verificar el resultado observable.
def test_cliente_oro_recibe_puntos_extra():
puntos = calcular_puntos(total=100, nivel="oro")
assert puntos == 210
Si luego refactorizamos la fórmula o movemos constantes a otro módulo, la prueba seguirá siendo válida mientras el comportamiento se mantenga.
Una prueba con muchas aserciones puede ser difícil de diagnosticar. Cuando falla, cuesta entender qué regla se rompió.
def test_transferencia():
origen = CuentaBancaria()
destino = CuentaBancaria()
origen.depositar(100)
origen.transferir_a(destino, 40)
assert origen.saldo == 60
assert destino.saldo == 40
assert origen.movimientos[-1].tipo == "transferencia_enviada"
assert destino.movimientos[-1].tipo == "transferencia_recibida"
Esta prueba no siempre está mal, pero puede mezclar dos reglas: saldos resultantes y movimientos registrados.
Podemos dividir la intención en dos pruebas más específicas.
def test_transferencia_mueve_saldo_entre_cuentas():
origen = CuentaBancaria()
destino = CuentaBancaria()
origen.depositar(100)
origen.transferir_a(destino, 40)
assert origen.saldo == 60
assert destino.saldo == 40
def test_transferencia_registra_movimientos_en_ambas_cuentas():
origen = CuentaBancaria()
destino = CuentaBancaria()
origen.depositar(100)
origen.transferir_a(destino, 40)
assert origen.movimientos[-1].tipo == "transferencia_enviada"
assert destino.movimientos[-1].tipo == "transferencia_recibida"
La duplicación de preparación puede resolverse con una fixture o una fábrica, siempre que no oculte la acción principal.
Durante TDD podemos usar preguntas simples en cada etapa.
No conviene borrar la prueba de inmediato. Primero hay que entender qué intención tenía.
python -m pytest.Revisá estas pruebas y detectá cuál es el problema de fragilidad en cada una.
def test_cuenta_con_saldo():
cuenta = CuentaBancaria()
cuenta.saldo = 100
assert cuenta.saldo == 100
def test_etiquetas():
assert obtener_etiquetas_compra(120, True) == [
"compra_grande",
"cliente_frecuente"
]
def test_descuento_hoy():
assert calcular_descuento_por_fecha(date.today()) == 10
Luego reescribí cada prueba para que sea más estable, más clara y más cercana al lenguaje del dominio.
Las pruebas frágiles reducen la utilidad de TDD porque hacen que el refactor sea costoso y confuso. Detectarlas a tiempo permite mantener una suite que protege el comportamiento importante sin imponer una forma rígida de implementación.
En el próximo tema trabajaremos sobre cómo refactorizar nombres, estructura y duplicación manteniendo todas las pruebas en verde.