Una prueba unitaria debe producir el mismo resultado cuando se ejecuta varias veces bajo las mismas condiciones. Si el código no cambió, la prueba no debería pasar algunas veces y fallar otras.
A esa propiedad la llamamos determinismo. Una prueba determinística es predecible: con los mismos datos y el mismo código, obtiene el mismo resultado.
En este tema veremos por qué algunas pruebas se vuelven variables y cómo hacerlas repetibles.
Una prueba determinística tiene estas características:
Una prueba intermitente, también llamada flaky, es una prueba que a veces pasa y a veces falla sin una causa visible inmediata.
Estas pruebas son peligrosas porque dañan la confianza en la suite. Si el equipo se acostumbra a fallas intermitentes, puede empezar a ignorar fallas reales.
Cuando una prueba es intermitente, no conviene aceptarla como "normal". Hay que buscar la fuente de variabilidad.
Una prueba puede fallar si depende de la fecha actual del sistema.
from datetime import date
def cupon_vigente(fecha_vencimiento):
return date.today() <= fecha_vencimiento
def test_cupon_vigente():
assert cupon_vigente(date(2026, 12, 31)) == True
Esta prueba pasará antes o durante el 31 de diciembre de 2026, pero fallará después. El resultado depende del día en que se ejecute.
Una forma de hacer la prueba determinística es pasar la fecha actual como parámetro.
from datetime import date
def cupon_vigente(fecha_actual, fecha_vencimiento):
return fecha_actual <= fecha_vencimiento
def test_cupon_vigente_en_fecha_anterior_al_vencimiento():
fecha_actual = date(2026, 6, 1)
fecha_vencimiento = date(2026, 12, 31)
assert cupon_vigente(fecha_actual, fecha_vencimiento) == True
def test_cupon_no_vigente_despues_del_vencimiento():
fecha_actual = date(2027, 1, 1)
fecha_vencimiento = date(2026, 12, 31)
assert cupon_vigente(fecha_actual, fecha_vencimiento) == False
Ahora la prueba no depende del reloj real. La fecha es un dato controlado.
Si una unidad usa valores aleatorios, la prueba puede recibir resultados distintos en cada ejecución.
import random
def generar_codigo():
return random.randint(1000, 9999)
def test_generar_codigo_tiene_cuatro_digitos():
codigo = generar_codigo()
assert codigo >= 1000 and codigo <= 9999
Esta prueba puede parecer estable, pero si la regla cambia o el rango se usa de forma más compleja, la aleatoriedad puede dificultar el diagnóstico. En general, conviene controlar el azar en pruebas unitarias.
Una opción es separar la generación aleatoria de la lógica que queremos probar.
def formato_codigo(numero):
return f"COD-{numero}"
def test_formato_codigo():
assert formato_codigo(1234) == "COD-1234"
La prueba verifica la lógica de formato con un dato fijo. La generación aleatoria puede probarse de otra manera o quedar fuera de la unidad principal.
Si una prueba usa datos modificados por otra, su resultado puede cambiar según el orden de ejecución.
usuarios = []
def test_agregar_usuario():
usuarios.append("Ana")
assert len(usuarios) == 1
Si otra prueba también modifica usuarios, el tamaño esperado puede no ser 1. La prueba deja de ser repetible.
def test_agregar_usuario():
usuarios = []
usuarios.append("Ana")
assert len(usuarios) == 1
La lista se crea dentro de la prueba. Cada ejecución empieza desde el mismo estado inicial.
Una prueba unitaria no debería depender de servicios externos como APIs, red, base de datos real o sistema de archivos externo. Esas dependencias pueden cambiar, fallar o tardar.
Problemas típicos:
Estos problemas hacen que la prueba falle por motivos ajenos a la unidad.
Si queremos probar una regla de negocio, conviene separarla de la dependencia externa.
def calcular_total_con_impuesto(precio, impuesto):
return precio + (precio * impuesto / 100)
def test_calcular_total_con_impuesto():
assert calcular_total_con_impuesto(1000, 21) == 1210
La prueba no necesita consultar una API ni una base de datos para verificar la fórmula. La integración con recursos externos se prueba en otro nivel.
Algunas estructuras no garantizan orden. Si una prueba exige un orden que la unidad no promete, puede volverse frágil.
def obtener_roles():
return {"admin", "editor"}
def test_obtener_roles():
roles = list(obtener_roles())
assert roles == ["admin", "editor"]
Un conjunto no garantiza orden. La prueba podría fallar si los elementos aparecen invertidos.
def test_obtener_roles():
roles = obtener_roles()
assert roles == {"admin", "editor"}
Ahora la prueba verifica el contenido sin imponer un orden innecesario.
Los cálculos con números decimales pueden producir pequeñas diferencias de precisión.
def test_promedio_decimal():
resultado = promedio([0.1, 0.2])
assert resultado == 0.15
Dependiendo del lenguaje y del tipo numérico, esta comparación exacta puede ser problemática. Si trabajamos con punto flotante, conviene usar tolerancia.
def test_promedio_decimal():
resultado = promedio([0.1, 0.2])
assert abs(resultado - 0.15) < 0.0001
La prueba acepta una diferencia muy pequeña. Esto hace que la verificación sea adecuada para el tipo de dato usado.
Una prueba repetible debería comportarse igual en la computadora de una persona, en la de otra y en un servidor de integración continua.
Factores que pueden variar entre entornos:
Las pruebas unitarias deben minimizar estas dependencias o controlarlas explícitamente.
Señales de alerta:
Estas señales indican que el resultado depende de algo externo al comportamiento probado.
| Causa | Riesgo | Solución |
|---|---|---|
| Fecha actual | La prueba cambia con el tiempo. | Pasar la fecha como dato controlado. |
| Aleatoriedad | Resultados distintos por ejecución. | Separar lógica o fijar entrada. |
| Estado compartido | Dependencia de orden. | Crear datos nuevos por prueba. |
| Dependencias externas | Fallas ajenas a la unidad. | Aislar la lógica o usar dobles de prueba. |
| Orden no garantizado | Fallas por comparación rígida. | Verificar contenido sin exigir orden innecesario. |
| Decimales | Diferencias pequeñas de precisión. | Usar tolerancia o tipos adecuados. |
Para revisar si una prueba es repetible, pregunta:
Las pruebas determinísticas y repetibles son esenciales para confiar en una suite unitaria. Si una prueba falla sin cambios en el código, deja de ser una señal clara.
El objetivo es controlar las condiciones de la prueba: datos, fechas, estado, orden y dependencias. Cuanto menos dependa del entorno, más útil será la prueba.
En el próximo tema veremos cómo evitar dependencias externas en pruebas unitarias.