6. Cuándo conviene escribir pruebas unitarias

6.1 Introducción

Después de entender qué son las pruebas unitarias, sus objetivos, sus beneficios y sus límites, aparece una pregunta práctica: ¿cuándo conviene escribirlas?

La respuesta no debería ser "siempre, para todo" ni "solo al final si queda tiempo". Las pruebas unitarias aportan más valor cuando se escriben en momentos adecuados y sobre comportamientos importantes.

En este tema veremos situaciones concretas en las que conviene escribir pruebas unitarias, momentos del desarrollo donde resultan especialmente útiles y casos donde quizá sea mejor elegir otro tipo de prueba.

6.2 Cuando hay una regla de negocio clara

Una de las mejores situaciones para escribir pruebas unitarias es cuando existe una regla de negocio concreta. Las reglas de negocio suelen contener decisiones, límites, excepciones o cálculos importantes.

Ejemplos:

  • Un cliente mayorista recibe descuento si compra más de 100 unidades.
  • Una persona puede registrarse si tiene al menos 18 años.
  • Un pedido se aprueba solo si tiene stock y el pago fue autorizado.
  • Un cupón vence después de determinada fecha.
  • Una transferencia no puede superar el saldo disponible.

Estas reglas merecen pruebas porque un error puede afectar directamente al comportamiento esperado del sistema.

6.3 Ejemplo de regla con límite

Supongamos que una compra obtiene envío gratis cuando el total es igual o superior a 5000.

def tiene_envio_gratis(total):
    return total >= 5000


def test_total_menor_a_5000_no_tiene_envio_gratis():
    assert tiene_envio_gratis(4999) == False


def test_total_igual_a_5000_tiene_envio_gratis():
    assert tiene_envio_gratis(5000) == True

Este es un buen momento para escribir pruebas unitarias porque el límite es importante. Un pequeño error, como usar > en lugar de >=, cambiaría la regla.

6.4 Antes de programar una funcionalidad

Una prueba unitaria puede escribirse antes de implementar el código. Esta práctica es parte del enfoque TDD, que estudiaremos en un curso específico, pero la idea básica es simple: primero expresamos el comportamiento esperado y luego escribimos el código que lo cumple.

Esto conviene cuando la regla está clara y podemos describirla mediante ejemplos concretos.

def test_descuento_para_cliente_vip():
    assert calcular_descuento(tipo_cliente="vip", monto=1000) == 150

Aunque la función calcular_descuento todavía no exista, la prueba expresa una expectativa: un cliente VIP obtiene 150 de descuento sobre una compra de 1000. Luego implementaremos el código necesario.

6.5 Durante el desarrollo

También conviene escribir pruebas unitarias mientras se desarrolla una funcionalidad. A medida que aparece una nueva regla o caso relevante, podemos agregar una prueba que la verifique.

Este enfoque es útil cuando aún estamos explorando la solución, pero ya conocemos algunos comportamientos importantes. La prueba ayuda a confirmar que cada pieza funciona antes de conectarla con el resto del sistema.

Por ejemplo, antes de integrar una regla de impuestos en una pantalla de facturación, podemos probar la función de cálculo directamente.

6.6 Después de encontrar un defecto

Una de las mejores oportunidades para agregar una prueba unitaria aparece cuando encontramos un defecto. Si el defecto se originó en una unidad de código, conviene escribir una prueba que lo reproduzca.

El proceso puede ser:

  1. Identificar el caso que falla.
  2. Escribir una prueba unitaria que falle por ese defecto.
  3. Corregir el código.
  4. Ejecutar la prueba y confirmar que ahora pasa.
  5. Dejar la prueba en la suite para evitar que el error vuelva.

Esta práctica convierte un defecto encontrado en una protección permanente contra regresiones.

6.7 Ejemplo de prueba agregada por un defecto

Supongamos que una función que calcula promedios falla con listas vacías. Al encontrar el defecto, podemos escribir una prueba que exprese el comportamiento esperado.

def promedio(numeros):
    if len(numeros) == 0:
        return 0
    return sum(numeros) / len(numeros)


def test_promedio_de_lista_vacia_es_cero():
    assert promedio([]) == 0

La prueba documenta una decisión: para una lista vacía, el promedio esperado es 0. Si en el futuro alguien modifica la función y rompe ese caso, la prueba lo señalará.

6.8 Antes de refactorizar

Antes de refactorizar código existente, conviene tener pruebas unitarias para los comportamientos importantes. Refactorizar sin pruebas puede ser riesgoso, porque podríamos cambiar accidentalmente lo que el código hace.

Si el código no tiene pruebas, una estrategia práctica es escribir algunas pruebas de caracterización. Estas pruebas registran el comportamiento actual antes de modificar la estructura interna.

No siempre necesitamos cubrir todo antes de refactorizar, pero sí conviene proteger las reglas más importantes y los casos que podrían romperse.

6.9 Al modificar código existente

Cuando debemos cambiar una funcionalidad existente, las pruebas unitarias ayudan a responder dos preguntas:

  • ¿El nuevo comportamiento funciona?
  • ¿Los comportamientos anteriores importantes siguen funcionando?

Si agregamos una regla nueva, conviene escribir una prueba para esa regla. Si tocamos código con reglas antiguas, conviene ejecutar o agregar pruebas que protejan los casos que no deberían cambiar.

6.10 Cuando hay casos límite

Los casos límite son excelentes candidatos para pruebas unitarias. Un caso límite aparece en el borde de una regla: justo antes, justo en el límite y justo después.

Ejemplos:

  • Edad mínima: 17, 18 y 19.
  • Stock disponible: 0, 1 y cantidad exacta solicitada.
  • Monto mínimo para descuento: 9999, 10000 y 10001.
  • Longitud mínima de contraseña: 7, 8 y 9 caracteres.

Estos valores suelen revelar errores en condiciones como >, >=, < o <=.

6.11 Ejemplo con longitud de contraseña

Supongamos que una contraseña debe tener al menos 8 caracteres.

def password_tiene_longitud_valida(password):
    return len(password) >= 8


def test_password_de_7_caracteres_no_es_valido():
    assert password_tiene_longitud_valida("abcdefg") == False


def test_password_de_8_caracteres_es_valido():
    assert password_tiene_longitud_valida("abcdefgh") == True

Este tipo de prueba es simple, rápida y útil. Protege una regla que podría romperse fácilmente por un cambio pequeño.

6.12 Cuando una unidad tiene varias ramas

Una unidad con condicionales suele necesitar pruebas para sus caminos principales. No se trata de probar combinaciones sin criterio, sino de cubrir decisiones relevantes.

def clasificar_temperatura(grados):
    if grados < 10:
        return "frio"
    if grados <= 25:
        return "templado"
    return "calor"


def test_temperatura_baja_es_frio():
    assert clasificar_temperatura(5) == "frio"


def test_temperatura_intermedia_es_templado():
    assert clasificar_temperatura(20) == "templado"


def test_temperatura_alta_es_calor():
    assert clasificar_temperatura(30) == "calor"

Cuando una función toma decisiones, cada rama importante debería tener al menos un caso representativo.

6.13 Cuando el comportamiento no es obvio

Si una regla puede generar dudas, una prueba unitaria ayuda a dejarla explícita. Esto ocurre con reglas de negocio, cálculos financieros, redondeos, prioridades o excepciones.

Por ejemplo, si un sistema redondea importes hacia arriba en algunos casos y hacia abajo en otros, conviene escribir pruebas que documenten esas decisiones.

Cuando alguien lea la prueba, no tendrá que adivinar la intención del código. Verá ejemplos concretos del comportamiento esperado.

6.14 Cuando una unidad es crítica

Una unidad crítica es aquella cuyo error puede tener impacto importante. Por ejemplo:

  • Cálculo de importes, impuestos, descuentos o intereses.
  • Validaciones de permisos.
  • Reglas de aprobación o rechazo.
  • Transformaciones de datos usadas por otros módulos.
  • Condiciones que afectan seguridad o consistencia.

Cuanto mayor es el impacto de una falla, más sentido tiene proteger la unidad con pruebas claras.

6.15 Cuando una unidad cambia con frecuencia

El código que cambia con frecuencia tiene más oportunidades de romperse. Si una unidad es modificada muchas veces, las pruebas unitarias ayudan a detectar regresiones.

Esto suele ocurrir en reglas de negocio activas, cálculos que se ajustan por nuevas condiciones, validadores que cambian por requisitos nuevos o transformadores de datos que deben adaptarse a formatos distintos.

En estas zonas del código, una prueba bien elegida puede ahorrar muchas revisiones manuales.

6.16 Cuando el defecto sería difícil de encontrar manualmente

Algunos errores no son fáciles de descubrir desde la interfaz. Pueden aparecer solo con ciertos datos, combinaciones o casos límite. Una prueba unitaria permite apuntar directamente a esos casos.

Ejemplo: una regla de comisión puede fallar solo cuando el importe es exactamente cero, o una conversión puede fallar solo con texto vacío. Probar esos casos manualmente en cada versión sería lento y fácil de olvidar.

Si un caso es importante pero molesto de verificar manualmente, suele ser buen candidato para automatización unitaria.

6.17 Cuándo quizá no conviene una prueba unitaria

No todo comportamiento necesita una prueba unitaria aislada. Puede no convenir cuando:

  • El código no tiene lógica propia y solo delega una llamada.
  • La prueba verificaría un detalle interno sin valor para el comportamiento externo.
  • La preparación del caso requiere levantar gran parte del sistema.
  • El riesgo principal está en la integración entre componentes.
  • La prueba repetiría exactamente la implementación.
  • El comportamiento se entiende mejor mediante una prueba de integración o end-to-end.

La decisión correcta no siempre es "más pruebas unitarias". Escribir pruebas útiles implica elegir el nivel adecuado para el riesgo que queremos cubrir.

6.18 Tabla de decisión rápida

Situación ¿Conviene prueba unitaria? Motivo
Regla de negocio con límites claros. Se puede verificar con casos concretos.
Cálculo de importes. Un error puede tener impacto directo.
Función que solo llama a otra función sin lógica propia. No siempre Puede no aportar información adicional.
Servicio que guarda en base de datos. Depende La lógica puede probarse unitariamente, pero la persistencia requiere integración.
Flujo completo de registro desde la interfaz. No Corresponde a prueba end-to-end o de interfaz.
Defecto encontrado en una función específica. La prueba evita que el defecto vuelva.

6.19 Orden práctico para decidir

Cuando dudes si escribir una prueba unitaria, puedes seguir este razonamiento:

  1. Identifica el comportamiento que quieres proteger.
  2. Busca la unidad donde vive ese comportamiento.
  3. Pregunta si puedes ejecutarla con datos controlados.
  4. Define el resultado esperado.
  5. Evalúa si la prueba será rápida y clara.
  6. Si necesitas muchas partes del sistema, considera una prueba de integración.

Este orden evita escribir pruebas por impulso y ayuda a elegir casos con intención.

6.20 Qué debes recordar de este tema

  • Conviene escribir pruebas unitarias para reglas claras, cálculos, validaciones y decisiones importantes.
  • También conviene agregarlas después de encontrar un defecto.
  • Son muy útiles antes de refactorizar o modificar código existente.
  • Los casos límite suelen ser buenos candidatos para pruebas unitarias.
  • Una unidad crítica o cambiante merece mayor atención.
  • No todo debe probarse unitariamente; a veces conviene integración o end-to-end.
  • La prueba debe tener una intención concreta y un resultado esperado claro.

6.21 Conclusión

Las pruebas unitarias convienen cuando permiten verificar un comportamiento importante de forma rápida, clara y aislada. Son especialmente útiles para reglas de negocio, cálculos, validaciones, defectos corregidos, casos límite y código que cambia con frecuencia.

También debemos reconocer cuándo no son la herramienta adecuada. Si el riesgo principal está en la colaboración entre componentes o en un flujo completo de usuario, probablemente necesitamos otro nivel de prueba.

En el próximo tema estudiaremos la anatomía de una prueba: preparación, ejecución y verificación.