Una de las decisiones más importantes al escribir pruebas unitarias es elegir qué vale la pena probar. No todo el código necesita una prueba directa, y no toda prueba que se puede escribir aporta valor real.
Una buena prueba unitaria verifica un comportamiento relevante de una unidad. En cambio, una prueba débil suele confirmar detalles obvios, copiar la implementación o depender demasiado de cómo el código está escrito por dentro.
Antes de escribir una prueba conviene hacer una pregunta simple:
¿Qué comportamiento observable debe cumplir esta unidad para que el sistema sea correcto?
La respuesta ayuda a separar lo importante de lo accidental. El comportamiento observable puede ser un valor devuelto, una excepción, un cambio de estado o una interacción esperada con una dependencia.
El objetivo de una prueba unitaria no es recorrer todas las líneas una por una, sino comprobar que la unidad cumple una responsabilidad concreta.
Por ejemplo, si una función calcula un precio final, lo importante es verificar el precio que entrega ante datos relevantes. No necesitamos probar cada variable temporal usada durante el cálculo.
En general, conviene probar aquello que representa una decisión del programa:
Las reglas de negocio suelen ser candidatas fuertes para pruebas unitarias, porque expresan condiciones propias del dominio del sistema.
def calcular_descuento(total):
if total >= 10000:
return total * 0.15
if total >= 5000:
return total * 0.10
return 0
def test_aplica_descuento_del_quince_por_ciento_para_compras_grandes():
assert calcular_descuento(12000) == 1800
Esta prueba es útil porque confirma una regla que podría cambiar, romperse o interpretarse mal.
Las funciones que calculan o transforman datos suelen ser fáciles de probar y aportan mucha confianza.
def normalizar_nombre(nombre):
return " ".join(nombre.strip().title().split())
def test_normaliza_espacios_y_mayusculas():
assert normalizar_nombre(" ana lopez ") == "Ana Lopez"
La prueba no se interesa por cada llamada interna a métodos de texto. Verifica el resultado final esperado.
Las validaciones deben probarse cuando protegen una regla importante, evitan datos inválidos o generan mensajes de error que el resto del sistema necesita interpretar.
def validar_edad(edad):
if edad < 0:
raise ValueError("La edad no puede ser negativa")
if edad < 18:
return "menor"
return "mayor"
def test_rechaza_edad_negativa():
try:
validar_edad(-1)
assert False
except ValueError as error:
assert str(error) == "La edad no puede ser negativa"
Los casos límite son valores ubicados justo en el borde de una condición. Suelen revelar errores frecuentes porque una comparación mal elegida cambia el comportamiento.
def es_mayor_de_edad(edad):
return edad >= 18
def test_dieciocho_es_mayor_de_edad():
assert es_mayor_de_edad(18) is True
def test_diecisiete_no_es_mayor_de_edad():
assert es_mayor_de_edad(17) is False
También conviene probar los errores esperados. Si una unidad debe rechazar cierto dato, la prueba debe dejar claro qué dato se rechaza y qué respuesta produce la unidad.
No se trata de forzar errores artificiales, sino de documentar los errores que forman parte del contrato de la unidad.
Si una unidad modifica su propio estado, conviene probar el cambio observable que importa para quien la usa.
class Carrito:
def __init__(self):
self.items = []
def agregar(self, producto):
self.items.append(producto)
def cantidad(self):
return len(self.items)
def test_agregar_producto_incrementa_la_cantidad():
carrito = Carrito()
carrito.agregar("teclado")
assert carrito.cantidad() == 1
La prueba se centra en el efecto visible: después de agregar un producto, la cantidad cambia.
A veces el comportamiento de una unidad consiste en comunicarse con otra pieza. En ese caso, puede ser válido probar que la interacción ocurre, siempre que esa interacción sea parte de la responsabilidad de la unidad.
class ServicioNotificaciones:
def __init__(self, enviador):
self.enviador = enviador
def avisar_compra(self, email):
self.enviador.enviar(email, "Compra confirmada")
class EnviadorFalso:
def __init__(self):
self.mensajes = []
def enviar(self, destino, texto):
self.mensajes.append((destino, texto))
def test_avisar_compra_envia_mensaje_de_confirmacion():
enviador = EnviadorFalso()
servicio = ServicioNotificaciones(enviador)
servicio.avisar_compra("ana@example.com")
assert enviador.mensajes == [("ana@example.com", "Compra confirmada")]
No todo merece una prueba directa. Algunas pruebas agregan mantenimiento, pero casi nada de confianza.
Como regla práctica, conviene evitar pruebas que solo confirmen detalles triviales, implementación interna, comportamiento de librerías o código que no contiene una decisión propia del programa.
Un método que solo devuelve o asigna un atributo sin lógica propia normalmente no necesita una prueba unitaria aislada.
class Usuario:
def __init__(self, nombre):
self.nombre = nombre
def obtener_nombre(self):
return self.nombre
Una prueba que solo comprueba que obtener_nombre devuelve nombre
suele aportar poco, salvo que ese método forme parte de una interfaz pública importante o
tenga una regla adicional.
Una prueba frágil se rompe cuando cambia la implementación aunque el comportamiento siga siendo correcto.
class Pedido:
def __init__(self):
self._items = []
def agregar(self, producto):
self._items.append(producto)
def total_items(self):
return len(self._items)
Es preferible probar total_items() después de agregar productos, en lugar de
verificar directamente el atributo interno _items.
Una prueba pierde claridad cuando repite el mismo algoritmo que está intentando verificar. En ese caso, la prueba puede fallar o pasar por las mismas razones que el código probado.
def calcular_total(precios):
return sum(precios)
def test_calcula_total():
precios = [100, 200, 300]
resultado = calcular_total(precios)
assert resultado == 600
La prueba usa un resultado esperado explícito. No conviene escribir
assert resultado == sum(precios), porque eso repite la misma idea que la unidad.
Las pruebas unitarias no deberían dedicarse a comprobar que una librería conocida funciona. Por ejemplo, no tiene sentido probar que una función estándar de ordenamiento ordena una lista.
Lo que sí puede probarse es cómo nuestra unidad usa el resultado de esa librería cuando hay una regla propia alrededor.
Si la prueba necesita base de datos real, red, sistema de archivos compartido o servicios externos, probablemente dejó de ser una prueba unitaria.
Eso no significa que esa prueba sea inútil. Significa que pertenece a otro nivel de testing. En este curso mantenemos clara la frontera para que las pruebas unitarias sean rápidas, aisladas y repetibles.
Comparemos una prueba que aporta poco con una prueba que expresa mejor la intención.
def aplicar_recargo(total, dias_atraso):
if dias_atraso <= 0:
return total
return total + dias_atraso * 50
def test_aplicar_recargo_devuelve_un_numero():
resultado = aplicar_recargo(1000, 2)
assert isinstance(resultado, int)
def test_aplica_cincuenta_pesos_por_dia_de_atraso():
resultado = aplicar_recargo(1000, 2)
assert resultado == 1100
La primera prueba solo confirma el tipo del resultado. La segunda verifica una regla concreta del comportamiento.
Esta tabla resume una forma práctica de decidir qué probar.
| Elemento | ¿Conviene probarlo? | Motivo |
|---|---|---|
| Regla de negocio | Sí | Define comportamiento importante del sistema. |
| Cálculo con condiciones | Sí | Puede fallar por límites o casos especiales. |
| Getter trivial | Generalmente no | No agrega lógica propia. |
| Detalle interno privado | No directamente | Vuelve frágil la prueba. |
| Librería externa | No | No es responsabilidad de nuestra unidad. |
| Error esperado por contrato | Sí | Documenta una condición inválida importante. |
Antes de escribir una prueba unitaria, revisa estas preguntas:
Una buena prueba unitaria no intenta probar todo. Intenta probar lo que importa. Debe enfocarse en comportamientos observables, reglas relevantes, cálculos, validaciones, errores esperados y cambios de estado significativos.
También es importante saber qué no probar: detalles internos, código trivial, librerías externas o implementaciones copiadas dentro de la prueba. Esta selección mejora la claridad, reduce mantenimiento y hace que la suite de pruebas sea más útil para el desarrollo diario.