2. Objetivos de una prueba unitaria

2.1 Introducción

En el tema anterior vimos que una prueba unitaria verifica una unidad pequeña de código. Ahora necesitamos responder una pregunta más práctica: ¿para qué escribimos una prueba unitaria?

La respuesta no es simplemente "para tener pruebas" ni "para cumplir con una métrica". Una prueba unitaria debe aportar información útil. Debe ayudarnos a saber si una parte del programa cumple una regla, si una modificación rompió algo importante o si el comportamiento esperado está expresado de forma clara.

Comprender los objetivos de una prueba unitaria es fundamental porque evita escribir pruebas mecánicas, frágiles o sin valor. Una buena prueba tiene una intención definida.

2.2 Objetivo principal

El objetivo principal de una prueba unitaria es verificar que una unidad de código se comporta como esperamos en una situación concreta.

Una prueba unitaria debe responder una pregunta específica sobre el comportamiento de una unidad.

Por ejemplo:

  • ¿La función devuelve el total correcto cuando se aplica un descuento?
  • ¿El método rechaza una contraseña demasiado corta?
  • ¿La clase impide retirar más dinero del saldo disponible?
  • ¿La regla de negocio aprueba un pedido cuando cumple todas las condiciones?
  • ¿La función lanza un error controlado cuando recibe datos inválidos?

Estas preguntas son concretas. Una prueba unitaria valiosa no intenta demostrar que "todo funciona"; se concentra en un comportamiento específico.

2.3 Verificar comportamiento, no implementación accidental

Un objetivo importante es comprobar el comportamiento observable de la unidad. Esto significa verificar lo que la unidad hace desde el punto de vista de quien la usa: qué devuelve, qué estado modifica o qué error produce.

No conviene probar detalles internos que podrían cambiar sin alterar el comportamiento real. Si una prueba depende demasiado de cómo está escrita la función por dentro, se vuelve frágil y puede fallar aunque el sistema siga funcionando correctamente.

def calcular_total(precio, impuesto):
    return precio + (precio * impuesto / 100)


def test_calcular_total_con_impuesto_del_21_por_ciento():
    assert calcular_total(1000, 21) == 1210

Esta prueba verifica el resultado esperado. No le importa si internamente la función usa una variable intermedia, una fórmula directa o una pequeña función auxiliar. Mientras el comportamiento sea correcto, la prueba debe pasar.

2.4 Detectar errores temprano

Uno de los grandes objetivos de las pruebas unitarias es encontrar errores cerca del momento en que se introducen. Cuando una prueba falla inmediatamente después de cambiar una función, el problema suele estar en ese cambio reciente.

Esto reduce el tiempo de búsqueda. Es distinto descubrir un error en una prueba unitaria pequeña que descubrirlo semanas después en una pantalla completa, con base de datos, sesión de usuario, red, permisos y varias reglas combinadas.

Las pruebas unitarias acortan la distancia entre causa y síntoma. Esa cercanía facilita el diagnóstico.

2.5 Proteger contra regresiones

Una regresión ocurre cuando algo que funcionaba correctamente deja de funcionar después de un cambio. Las regresiones son muy comunes en software: corregimos una regla, agregamos una condición, refactorizamos un módulo o cambiamos una dependencia, y sin querer rompemos un comportamiento existente.

Una prueba unitaria ayuda a proteger comportamientos importantes para que no se pierdan accidentalmente.

def aplicar_descuento(precio, porcentaje):
    return precio - (precio * porcentaje / 100)


def test_aplicar_descuento_no_modifica_precio_con_descuento_cero():
    assert aplicar_descuento(500, 0) == 500


def test_aplicar_descuento_del_20_por_ciento():
    assert aplicar_descuento(500, 20) == 400

Si alguien modifica la función y rompe alguno de esos casos, las pruebas lo indican. La suite de pruebas actúa como memoria automática de comportamientos que el sistema debe conservar.

2.6 Documentar reglas con ejemplos ejecutables

Una prueba unitaria bien escrita también cumple una función de documentación. No documenta con una explicación larga, sino con un ejemplo concreto que se puede ejecutar.

Consideremos una regla simple: un usuario puede acceder a una promoción si es cliente activo y tiene al menos 100 puntos.

def puede_acceder_a_promocion(cliente_activo, puntos):
    return cliente_activo and puntos >= 100


def test_cliente_activo_con_100_puntos_puede_acceder_a_promocion():
    assert puede_acceder_a_promocion(True, 100) == True


def test_cliente_inactivo_no_puede_acceder_a_promocion():
    assert puede_acceder_a_promocion(False, 200) == False

Al leer estas pruebas, una persona entiende parte de la regla de negocio. Además, puede ejecutarlas para comprobar que la regla sigue vigente.

2.7 Aumentar la confianza para cambiar código

El software cambia constantemente. Se agregan funcionalidades, se corrigen defectos, se optimiza rendimiento y se refactoriza código. Sin pruebas, cada cambio importante genera incertidumbre: no sabemos qué se rompió hasta probar manualmente muchas cosas o hasta que alguien reporte un problema.

Las pruebas unitarias no eliminan todo riesgo, pero aumentan la confianza. Si después de modificar una unidad ejecutamos sus pruebas y todas pasan, tenemos evidencia de que los comportamientos cubiertos siguen funcionando.

Esta confianza es especialmente importante al refactorizar. Refactorizar significa mejorar la estructura interna del código sin cambiar su comportamiento observable. Las pruebas unitarias ayudan a comprobar que ese comportamiento se mantiene.

2.8 Facilitar el diseño de unidades más simples

Otro objetivo de las pruebas unitarias es revelar cuándo una unidad es difícil de usar o de verificar. Si cuesta mucho escribir una prueba, puede ser una señal de que la unidad tiene demasiadas responsabilidades o depende de demasiados recursos externos.

Por ejemplo, una función que calcula un importe es fácil de probar si recibe números y devuelve un número. Pero si además lee una base de datos, consulta una API, revisa la fecha del sistema y escribe en un archivo, deja de ser una unidad simple.

En ese sentido, las pruebas unitarias funcionan como una presión positiva sobre el diseño: favorecen funciones, clases y módulos con responsabilidades claras.

2.9 Reducir la necesidad de depuración manual

La depuración manual es útil, pero puede volverse lenta si dependemos de ella para verificar cada cambio. Ejecutar la aplicación, navegar hasta una pantalla, cargar datos y observar el resultado puede tomar bastante tiempo.

Una prueba unitaria permite comprobar una regla directamente. Si queremos saber si una función calcula bien una comisión, no necesitamos iniciar toda la aplicación. Podemos ejecutar una prueba que llame a esa función con datos controlados.

Esto no significa que nunca debamos probar manualmente. Significa que muchas verificaciones pequeñas y repetibles pueden automatizarse a nivel unitario, dejando la exploración manual para otros objetivos.

2.10 Comprobar casos normales, límites y errores

Una unidad de código no debe probarse solo con el caso más cómodo. Otro objetivo de las pruebas unitarias es cubrir situaciones representativas:

  • Casos normales: entradas habituales que deberían funcionar.
  • Casos límite: valores en los bordes de una regla.
  • Casos inválidos: entradas que deben rechazarse o manejarse de forma controlada.
  • Casos especiales: valores como cero, listas vacías, textos vacíos o ausencia de datos.

Ejemplo con una validación de edad:

def puede_registrarse(edad):
    return edad >= 18


def test_edad_17_no_puede_registrarse():
    assert puede_registrarse(17) == False


def test_edad_18_puede_registrarse():
    assert puede_registrarse(18) == True


def test_edad_19_puede_registrarse():
    assert puede_registrarse(19) == True

Los casos 17, 18 y 19 son valiosos porque rodean el límite de la regla. En temas posteriores veremos técnicas más formales para elegir estos casos.

2.11 Aislar el problema cuando algo falla

Cuando una prueba unitaria falla, debería señalar un problema bastante específico. Esa es una de sus ventajas frente a pruebas más grandes: el área de búsqueda es menor.

Si falla una prueba llamada test_edad_18_puede_registrarse, sabemos que debemos revisar la regla que determina la mayoría de edad o los datos usados por esa prueba. En cambio, si falla un flujo completo de registro, el error podría estar en la interfaz, el formulario, la validación, el servicio, la base de datos o la navegación.

Por eso conviene que cada prueba unitaria tenga una intención clara y un nombre descriptivo. Cuando falla, debe ayudarnos a entender qué comportamiento se rompió.

2.12 Evitar objetivos equivocados

No todo lo que se puede medir o automatizar es un buen objetivo para una prueba unitaria. Algunos objetivos suelen producir pruebas de baja calidad:

  • Probar por cantidad: escribir muchas pruebas sin intención clara.
  • Perseguir cobertura sin criterio: ejecutar líneas de código sin verificar comportamientos importantes.
  • Duplicar implementación: escribir una prueba que repite exactamente la misma lógica del código probado.
  • Probar detalles privados innecesarios: acoplar la prueba a decisiones internas que podrían cambiar.
  • Verificar todo en una sola prueba: crear pruebas difíciles de leer y de diagnosticar.
Una prueba unitaria no debe existir solo para aumentar un número. Debe proteger o explicar un comportamiento importante.

2.13 Relación con la cobertura de código

La cobertura de código indica qué partes del código fueron ejecutadas por las pruebas. Puede ser una métrica útil, pero no debe confundirse con calidad de pruebas.

Una prueba puede ejecutar una línea de código y aun así no verificar correctamente el resultado. Por ejemplo:

def test_prueba_sin_verificacion_real():
    calcular_total(1000, 21)

Esta prueba aumenta la ejecución de código, pero no comprueba si el total es correcto. Una versión con mejor objetivo sería:

def test_calcular_total_con_impuesto_del_21_por_ciento():
    assert calcular_total(1000, 21) == 1210

La cobertura será estudiada con más detalle en otro curso. En este curso la usaremos como apoyo, no como sustituto del criterio para elegir buenos casos.

2.14 Objetivos según el tipo de unidad

El objetivo de la prueba puede cambiar según la unidad que estamos verificando:

Tipo de unidad Objetivo habitual de la prueba Ejemplo
Función de cálculo Comprobar el valor devuelto. Calcular un impuesto o descuento.
Validador Aceptar datos válidos y rechazar datos inválidos. Validar una edad, un correo o una contraseña.
Clase con estado Verificar cambios de estado después de una operación. Depositar dinero en una cuenta.
Regla de negocio Comprobar decisiones según condiciones. Aprobar o rechazar un pedido.
Transformador de datos Verificar que la salida tenga la forma esperada. Convertir un registro a un diccionario.

2.15 Cómo reconocer una prueba con buen objetivo

Una prueba tiene un buen objetivo cuando podemos responder con claridad estas preguntas:

  • ¿Qué unidad se está probando?
  • ¿Qué situación concreta se está verificando?
  • ¿Cuál es el resultado esperado?
  • ¿Qué comportamiento se protege si la prueba queda en la suite?
  • ¿Qué información aporta si falla?

Si no podemos responder estas preguntas, probablemente la prueba necesita ser reformulada, dividida o eliminada.

2.16 Ejemplo: prueba con objetivo confuso

Veamos una prueba poco recomendable:

def test_usuario():
    usuario = crear_usuario("Ana", 20)
    assert usuario.nombre == "Ana"
    assert usuario.edad == 20
    assert usuario.puede_registrarse() == True
    assert usuario.obtener_categoria() == "adulto"

Esta prueba mezcla varias ideas: creación del usuario, almacenamiento de datos, regla de registro y categoría. Si falla, tendremos que mirar cuál de todas esas expectativas se rompió.

Una alternativa más clara es separar objetivos:

def test_crear_usuario_guarda_nombre_y_edad():
    usuario = crear_usuario("Ana", 20)

    assert usuario.nombre == "Ana"
    assert usuario.edad == 20


def test_usuario_mayor_de_edad_puede_registrarse():
    usuario = crear_usuario("Ana", 20)

    assert usuario.puede_registrarse() == True

Ahora cada prueba comunica mejor su intención. Si una falla, el diagnóstico será más directo.

2.17 Pruebas como herramienta de comunicación

Las pruebas unitarias no solo sirven a la computadora. También sirven a las personas que leen y mantienen el código.

Una prueba con buen nombre y buen ejemplo comunica una regla de manera precisa. Puede ayudar a un nuevo integrante del equipo a entender cómo debe comportarse una función o qué casos son importantes para una regla de negocio.

Por eso conviene escribir pruebas pensando en quien las leerá después. Una prueba clara ahorra explicaciones y reduce malentendidos.

2.18 Qué debes recordar de este tema

  • El objetivo principal de una prueba unitaria es verificar un comportamiento específico de una unidad.
  • Las pruebas unitarias ayudan a detectar errores temprano.
  • También protegen contra regresiones cuando el código cambia.
  • Una buena prueba puede funcionar como documentación ejecutable.
  • La prueba debe verificar comportamiento observable, no detalles internos innecesarios.
  • La cobertura de código no reemplaza una buena elección de casos.
  • Una prueba con buen objetivo ayuda a diagnosticar rápidamente qué se rompió.

2.19 Conclusión

Escribir pruebas unitarias no consiste en acumular archivos de prueba ni en aumentar una métrica sin analizar su utilidad. Una prueba unitaria debe tener un objetivo claro: verificar una regla, proteger un comportamiento, documentar un ejemplo importante o dar confianza para modificar código.

Cuando una prueba tiene una intención concreta, se vuelve más fácil de leer, más fácil de mantener y más útil cuando falla.

En el próximo tema estudiaremos con mayor precisión qué entendemos por unidad de código, porque elegir bien la unidad es una condición importante para escribir buenas pruebas unitarias.