1. ¿Qué son las Pruebas Unitarias?

1.1 Introducción

Las pruebas unitarias son pruebas automatizadas que verifican el comportamiento de una parte pequeña y concreta de un programa. Esa parte suele ser una función, un método, una clase o un módulo con una responsabilidad bien definida.

Cuando escribimos una prueba unitaria, no intentamos comprobar toda la aplicación. Nos enfocamos en una unidad de código y formulamos una pregunta precisa: si esta unidad recibe estos datos o se ejecuta bajo estas condiciones, ¿produce el resultado esperado?

Este nivel de prueba es especialmente importante para quienes programan, porque permite detectar errores cerca del lugar donde se originan. Si una función calcula mal un descuento, valida mal un dato o interpreta incorrectamente una regla de negocio, una prueba unitaria bien escrita puede mostrar el problema de forma rápida y clara.

1.2 Una definición simple

Podemos definir una prueba unitaria de esta manera:

Una prueba unitaria es una verificación automatizada, pequeña y repetible que comprueba si una unidad de código se comporta como se espera en un caso específico.

Esta definición contiene varias ideas importantes:

  • Automatizada: se ejecuta mediante una herramienta o framework de pruebas, no manualmente paso a paso.
  • Pequeña: se concentra en una porción reducida del código.
  • Repetible: debe poder ejecutarse muchas veces y producir el mismo resultado si el código no cambió.
  • Específica: comprueba una situación concreta, no una lista confusa de comportamientos mezclados.
  • Verificable: compara un resultado obtenido con un resultado esperado.

1.3 Qué significa "unidad"

La palabra unidad no siempre significa exactamente lo mismo en todos los proyectos. En general, se refiere a una parte pequeña del sistema que puede probarse de manera aislada o casi aislada.

Algunos ejemplos habituales de unidades son:

  • Una función que calcula el total de una compra.
  • Un método que valida si una contraseña cumple ciertas reglas.
  • Una clase que representa una cuenta bancaria y permite depositar o extraer dinero.
  • Un módulo que convierte datos de un formato a otro.
  • Una regla de negocio que decide si un pedido puede ser aprobado.

Lo importante no es el tamaño exacto en líneas de código, sino que la unidad tenga una responsabilidad clara y que podamos verificar su comportamiento sin depender de toda la aplicación completa.

1.4 Primer ejemplo conceptual

Supongamos que tenemos una función encargada de calcular el precio final de un producto aplicando un descuento. La regla es simple: si el precio es 1000 y el descuento es 10%, el resultado debe ser 900.

def calcular_precio_final(precio, descuento):
    return precio - (precio * descuento / 100)


def test_calcular_precio_final_con_descuento_del_10_por_ciento():
    resultado = calcular_precio_final(1000, 10)
    assert resultado == 900

Este ejemplo contiene los elementos básicos de una prueba unitaria:

  • Se elige una unidad: la función calcular_precio_final.
  • Se prepara un caso concreto: precio 1000 y descuento 10.
  • Se ejecuta la unidad con esos datos.
  • Se verifica el resultado esperado mediante una aserción.

Más adelante estudiaremos con detalle la estructura de una prueba, pero desde el comienzo conviene observar que una prueba unitaria debe ser clara incluso para quien la lee por primera vez.

1.5 La idea de resultado esperado

Una prueba unitaria necesita un resultado esperado. Sin ese resultado, solo estamos ejecutando código, pero no estamos comprobando nada.

Por ejemplo, si una función suma dos números, no alcanza con llamarla. Debemos verificar que la salida sea correcta:

def sumar(a, b):
    return a + b


def test_sumar_dos_numeros_positivos():
    assert sumar(2, 3) == 5

La línea con assert expresa una expectativa: esperamos que la suma de 2 y 3 sea 5. Si el resultado obtenido es diferente, la prueba falla y nos avisa que existe un problema.

1.6 Qué comprueba una prueba unitaria

Una prueba unitaria puede comprobar distintos aspectos de una unidad de código. Los más frecuentes son:

  • Valores de retorno: una función recibe datos y devuelve el valor correcto.
  • Cambios de estado: un objeto modifica sus propiedades de la manera esperada.
  • Errores esperados: una unidad rechaza datos inválidos lanzando una excepción o devolviendo un error controlado.
  • Reglas de negocio: una decisión del sistema se toma según las condiciones definidas.
  • Interacciones básicas: una unidad colabora con una dependencia de forma prevista, cuando el diseño lo requiere.

En este primer tema no necesitamos dominar todos esos casos. Lo importante es entender que una prueba unitaria verifica comportamientos observables de una unidad, no detalles irrelevantes de implementación.

1.7 Pruebas unitarias y pruebas manuales

Una prueba manual requiere que una persona ejecute pasos y observe el resultado. Por ejemplo, abrir una pantalla, completar un formulario y verificar visualmente un mensaje.

Una prueba unitaria, en cambio, se ejecuta como código. Esto permite repetirla muchas veces, integrarla al proceso de desarrollo y ejecutarla automáticamente cada vez que modificamos el proyecto.

Aspecto Prueba manual Prueba unitaria
Ejecución La realiza una persona. La ejecuta una herramienta de pruebas.
Alcance Puede abarcar pantallas, flujos y experiencia de usuario. Se concentra en unidades pequeñas de código.
Velocidad Depende del tiempo humano. Suele ser muy rápida.
Repetición Puede volverse lenta y costosa. Está pensada para repetirse con frecuencia.

1.8 Diferencia con otros niveles de prueba

Las pruebas unitarias forman parte de una estrategia de testing más amplia. No reemplazan a otros tipos de pruebas; cumplen una función distinta.

Nivel de prueba Qué verifica Ejemplo
Prueba unitaria Una unidad pequeña de código. Una función calcula correctamente un impuesto.
Prueba de integración La colaboración entre partes del sistema. Un servicio guarda correctamente datos en una base de datos.
Prueba end-to-end Un flujo completo desde la perspectiva del usuario. Un usuario compra un producto desde el carrito hasta el pago.

Una prueba unitaria puede decirnos que una regla de cálculo funciona. Una prueba de integración puede decirnos que esa regla se usa correctamente junto con la base de datos. Una prueba end-to-end puede decirnos que el usuario puede completar el flujo completo en la aplicación.

1.9 Por qué son importantes

Las pruebas unitarias aportan valor porque permiten descubrir problemas temprano. Cuanto más cerca estamos del código que falló, más fácil suele ser encontrar la causa y corregirla.

Algunos beneficios concretos son:

  • Retroalimentación rápida: avisan en segundos si una unidad dejó de comportarse como se esperaba.
  • Mayor confianza al cambiar código: ayudan a detectar regresiones después de una modificación.
  • Documentación viva: muestran ejemplos concretos de cómo se espera que se comporte una unidad.
  • Mejor diseño: obligan a escribir código más claro, con responsabilidades más pequeñas y dependencias más controladas.
  • Menos depuración manual: reducen la necesidad de probar siempre todo desde la interfaz de usuario.

Una suite de pruebas unitarias bien cuidada se convierte en una red de verificación permanente para el proyecto.

1.10 Qué problemas pueden detectar

Las pruebas unitarias son especialmente útiles para detectar errores en lógica interna. Por ejemplo:

  • Cálculos incorrectos.
  • Validaciones incompletas.
  • Condiciones mal escritas.
  • Errores en casos límite.
  • Reglas de negocio mal interpretadas.
  • Transformaciones de datos defectuosas.
  • Comportamientos que se rompen después de refactorizar.

Si una regla dice que una persona menor de 18 años no puede registrarse en cierto servicio, una prueba unitaria puede verificar de forma directa qué ocurre con edades como 17, 18 y 19.

1.11 Qué no resuelven por sí solas

Es importante conocer los límites de las pruebas unitarias. Una aplicación puede tener muchas pruebas unitarias exitosas y aun así fallar cuando sus partes trabajan juntas.

Las pruebas unitarias no garantizan por sí solas que:

  • La interfaz de usuario sea clara o accesible.
  • La aplicación funcione correctamente con una base de datos real.
  • Los servicios externos respondan como esperamos.
  • Un flujo completo de usuario esté correctamente conectado.
  • El sistema tenga buen rendimiento bajo carga.
  • La aplicación cumpla todas las necesidades del negocio.

Por eso se complementan con otros niveles de prueba. En este curso aprenderemos a usarlas bien dentro de su alcance, sin esperar que resuelvan todos los problemas de calidad del software.

1.12 Características de una buena prueba unitaria

Una buena prueba unitaria no es solamente una prueba que pasa. También debe ser fácil de entender, mantener y ejecutar.

  • Clara: su intención se entiende al leer el nombre y el cuerpo de la prueba.
  • Rápida: se ejecuta en poco tiempo para que pueda correrse con frecuencia.
  • Aislada: no depende innecesariamente de otras pruebas ni de recursos externos.
  • Determinística: si el código no cambió, debe producir siempre el mismo resultado.
  • Focalizada: verifica una idea principal por vez.
  • Mantenible: no se rompe por detalles internos irrelevantes.
Idea clave: una prueba unitaria debe ayudar a entender el comportamiento esperado de una unidad, no convertirse en una dificultad adicional para el equipo.

1.13 Aserciones: el corazón de la prueba

Una aserción es la comprobación que decide si la prueba pasa o falla. En muchos lenguajes y frameworks aparece con palabras como assert, expect, should o métodos similares.

Por ejemplo:

def es_mayor_de_edad(edad):
    return edad >= 18


def test_una_persona_de_18_anios_es_mayor_de_edad():
    assert es_mayor_de_edad(18) == True

La aserción indica exactamente qué esperamos. Si la función devuelve False para la edad 18, la prueba falla y señala que la regla no se está cumpliendo.

Más adelante veremos cómo escribir aserciones más expresivas y cómo evitar pruebas que ejecutan código pero no verifican nada importante.

1.14 Una prueba debe poder fallar

Una prueba que nunca puede fallar no aporta información. Por ejemplo, ejecutar una función sin comprobar su resultado no verifica el comportamiento de la unidad.

def test_ejemplo_poco_util():
    calcular_precio_final(1000, 10)

Este ejemplo llama a la función, pero no dice qué resultado espera. Salvo que la función produzca un error durante la ejecución, la prueba podría pasar aunque el cálculo sea incorrecto.

Una versión útil sería:

def test_calcular_precio_final_con_descuento_del_10_por_ciento():
    assert calcular_precio_final(1000, 10) == 900

La diferencia es fundamental: ahora la prueba tiene una expectativa concreta y puede detectar un resultado incorrecto.

1.15 Cuándo se escriben las pruebas unitarias

Las pruebas unitarias pueden escribirse en distintos momentos del desarrollo. Lo importante es que acompañen al código y no queden como una tarea olvidada para el final.

  • Antes de programar la solución: como ocurre en TDD, cuando primero se expresa el comportamiento esperado mediante una prueba.
  • Durante el desarrollo: para verificar cada regla a medida que se implementa.
  • Después de encontrar un defecto: para reproducir el error y evitar que vuelva a aparecer.
  • Antes de refactorizar: para tener mayor confianza al mejorar la estructura del código.
  • Al modificar una funcionalidad existente: para proteger comportamientos importantes.

En este curso no estudiaremos TDD en profundidad, porque tendrá un curso propio. Sin embargo, sí veremos por qué las pruebas unitarias son una base importante para poder trabajar con ese enfoque.

1.16 Herramientas habituales

Cada lenguaje de programación suele tener frameworks o bibliotecas para escribir y ejecutar pruebas unitarias. Algunos ejemplos conocidos son:

Lenguaje Herramientas frecuentes
Python unittest, pytest
Java JUnit, TestNG
JavaScript Jest, Vitest, Mocha
C# xUnit, NUnit, MSTest
PHP PHPUnit

Los detalles de sintaxis cambian según el lenguaje, pero la idea central se mantiene: preparar un caso, ejecutar la unidad y verificar el resultado esperado.

1.17 Relación con el diseño del código

Las pruebas unitarias suelen revelar problemas de diseño. Si una función es muy larga, mezcla muchas responsabilidades o depende directamente de archivos, bases de datos, red, fechas del sistema y configuraciones globales, será más difícil probarla de manera unitaria.

En cambio, el código con responsabilidades claras suele ser más fácil de probar. Por ejemplo, una función que solo calcula un resultado a partir de datos de entrada es mucho más simple de verificar que una función que además lee archivos, consulta una API, escribe logs y modifica variables globales.

Por eso las pruebas unitarias no solo detectan errores: también nos ayudan a descubrir cuándo el código necesita una estructura más clara.

1.18 Errores comunes al comenzar

Cuando alguien empieza a escribir pruebas unitarias, es normal cometer algunos errores. Los más frecuentes son:

  • Probar demasiadas cosas en una sola prueba.
  • Escribir nombres que no explican qué comportamiento se verifica.
  • Depender de una base de datos, un archivo o una API externa sin necesidad.
  • Copiar mucha preparación repetida en cada prueba.
  • Verificar detalles internos en lugar de resultados observables.
  • Crear pruebas que pasan, pero que no comprueban nada importante.
  • Abandonar las pruebas cuando cambian los requisitos.

Estos problemas se corrigen con práctica y criterio. A lo largo del curso veremos técnicas para escribir pruebas más simples, expresivas y mantenibles.

1.19 Qué debes recordar de este tema

  • Una prueba unitaria verifica una unidad pequeña de código.
  • Debe tener un resultado esperado claro.
  • Normalmente se ejecuta de forma automática con una herramienta de pruebas.
  • Sirve para detectar errores temprano y dar confianza al modificar código.
  • No reemplaza a las pruebas de integración, end-to-end ni manuales.
  • Una buena prueba unitaria es clara, rápida, aislada, determinística y focalizada.
  • Las aserciones son las comprobaciones que determinan si la prueba pasa o falla.

1.20 Conclusión

Las pruebas unitarias son una herramienta fundamental para construir software más confiable. Permiten verificar pequeñas partes del código de manera rápida y repetible, detectar errores cerca de su origen y documentar con ejemplos el comportamiento esperado de una unidad.

Para quien comienza, la idea principal es esta: una prueba unitaria ejecuta una unidad de código con condiciones conocidas y comprueba que el resultado coincida con lo esperado.

En los próximos temas profundizaremos en los objetivos de una prueba unitaria, qué consideramos una unidad, cómo se estructura una prueba y cómo elegir casos que realmente aporten valor.