32. Qué probar y qué no probar en una unidad

32.1 Introducción

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.

32.2 La pregunta central

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.

32.3 Probar resultados, no líneas de código

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.

32.4 Qué sí conviene probar

En general, conviene probar aquello que representa una decisión del programa:

  • Reglas de negocio.
  • Cálculos.
  • Validaciones.
  • Transformaciones de datos.
  • Casos límite.
  • Errores esperados.
  • Cambios de estado importantes.
  • Interacciones necesarias con dependencias.

32.5 Reglas de negocio

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.

32.6 Cálculos y transformaciones

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.

32.7 Validaciones

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"

32.8 Casos límite

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

32.9 Errores esperados

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.

32.10 Cambios de estado relevantes

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.

32.11 Interacciones con dependencias

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")]

32.12 Qué no conviene probar

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.

32.13 No probar getters y setters triviales

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.

32.14 No probar detalles internos

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.

32.15 No duplicar la implementación en la prueba

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.

32.16 No probar librerías externas como si fueran propias

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.

32.17 No convertir una prueba unitaria en una prueba de integración

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.

32.18 Prueba débil contra prueba útil

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.

32.19 Tabla de decisión

Esta tabla resume una forma práctica de decidir qué probar.

Elemento ¿Conviene probarlo? Motivo
Regla de negocio Define comportamiento importante del sistema.
Cálculo con condiciones 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 Documenta una condición inválida importante.

32.20 Lista de comprobación

Antes de escribir una prueba unitaria, revisa estas preguntas:

  • ¿Estoy probando un comportamiento observable?
  • ¿La prueba explica una regla, un cálculo o una condición importante?
  • ¿El resultado esperado está escrito de forma clara?
  • ¿La prueba evita depender de atributos internos innecesarios?
  • ¿La prueba puede ejecutarse sin servicios externos?
  • ¿La prueba seguiría teniendo sentido si refactorizo la implementación?

32.21 Conclusión

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.