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.
Podemos definir una prueba unitaria de esta manera:
Esta definición contiene varias ideas importantes:
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:
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.
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:
calcular_precio_final.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.
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.
Una prueba unitaria puede comprobar distintos aspectos de una unidad de código. Los más frecuentes son:
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.
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. |
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.
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:
Una suite de pruebas unitarias bien cuidada se convierte en una red de verificación permanente para el proyecto.
Las pruebas unitarias son especialmente útiles para detectar errores en lógica interna. Por ejemplo:
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.
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:
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.
Una buena prueba unitaria no es solamente una prueba que pasa. También debe ser fácil de entender, mantener y ejecutar.
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.
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.
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.
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.
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.
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.
Cuando alguien empieza a escribir pruebas unitarias, es normal cometer algunos errores. Los más frecuentes son:
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.
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.