Las pruebas unitarias son la forma más directa de saber si una función hace lo que debería hacer. No requieren un servidor en ejecución, ni una base de datos poblada, ni un navegador abierto: solo el código y sus entradas y salidas. Por eso son rápidas, confiables y fundamentales en cualquier proyecto de software que aspire a sobrevivir a más de un ciclo de desarrollo.
Qué es exactamente una prueba unitaria
Una prueba unitaria es un fragmento de código que llama a una unidad del sistema —una función, un método, o una clase— con entradas conocidas y verifica que las salidas o efectos sean los esperados. La palabra "unitaria" implica aislamiento: la prueba no debe depender de bases de datos externas, APIs de terceros, sistemas de archivos ni ningún recurso que no sea la propia unidad bajo prueba.
Cuando es necesario que la unidad interactúe con dependencias externas (por ejemplo, una función que consulta una base de datos), se utilizan mocks o stubs para simular esas dependencias con comportamiento predecible y controlado. Esto garantiza que si una prueba falla, el problema está en la función bajo prueba, no en una dependencia externa que se comportó de forma inesperada.
Por qué las pruebas unitarias son valiosas más allá de detectar bugs
Detectar bugs es el beneficio más obvio, pero las pruebas unitarias aportan mucho más. Sirven como documentación ejecutable: cualquier desarrollador que se una al equipo puede leer una prueba unitaria y entender exactamente qué comportamiento se espera de una función, con qué entradas y con qué resultados. Son más precisas que un comentario y siempre están actualizadas porque, si el código cambia y la prueba falla, alguien tiene que decidir si el comportamiento nuevo es correcto o no.
También influyen directamente en el diseño del código. Una función difícil de probar unitariamente suele ser una función con demasiadas responsabilidades o con dependencias mal gestionadas. Cuando un desarrollador se pregunta "¿cómo voy a probar esto?", está haciendo exactamente la misma pregunta que llevaría a un mejor diseño: separación de responsabilidades, inyección de dependencias, funciones puras.
Efecto colateral positivo: El código bien pensado para ser probado unitariamente es también el código más fácil de entender, de reutilizar y de modificar en el futuro. Las pruebas y la buena arquitectura se refuerzan mutuamente.
Cómo escribir una buena prueba unitaria
Una buena prueba unitaria sigue el patrón AAA: Arrange (preparar el estado inicial y las dependencias), Act (ejecutar la función bajo prueba), Assert (verificar que el resultado es el esperado). Este patrón hace la prueba fácil de leer y mantener porque separa claramente las tres responsabilidades.
El nombre de la prueba debe describir el comportamiento que se verifica, no el nombre del método. En lugar de test_calcular_descuento, un nombre como test_cliente_premium_recibe_20_pct_de_descuento comunica el escenario y el resultado esperado directamente. Cuando la prueba falla, el nombre ya es un diagnóstico parcial.
Una prueba, una responsabilidad
Cada test debe verificar exactamente una cosa. Si una prueba puede fallar por dos razones distintas, es en realidad dos pruebas mezcladas. Sepáralas.
Pruebas deterministas, no flaky
Una prueba que pasa unas veces y falla otras es peor que no tenerla: genera desconfianza en todo el suite. Si una prueba depende del tiempo, del orden de ejecución o de datos externos, aíslala con mocks.
Cubre casos borde, no solo el camino feliz
El camino normal ya funciona casi siempre. Los bugs aparecen en entradas vacías, valores nulos, números negativos, strings con caracteres especiales. Prueba los extremos.
TDD: escribir la prueba antes que el código
Test-Driven Development (TDD) invierte el orden convencional: primero se escribe la prueba que define el comportamiento esperado (que fallará porque el código aún no existe), luego se escribe el código mínimo necesario para que la prueba pase, y finalmente se refactoriza el código manteniendo las pruebas en verde. Este ciclo —Red, Green, Refactor— es el corazón de TDD.
La ventaja principal de TDD no es la cobertura de pruebas al final: es el diseño del sistema que emerge de pensar primero en las interfaces antes que en la implementación. Los sistemas diseñados con TDD tienden a tener funciones más cohesivas, dependencias más explícitas y menos código muerto o innecesario.
Errores comunes al escribir pruebas unitarias
Probar los detalles de implementación en lugar del comportamiento observable. Si cambias el nombre interno de una variable y la prueba falla, la prueba estaba probando lo incorrecto.
Crear pruebas que solo pasan con datos perfectos. Un sistema real recibe entradas inesperadas. Prueba con datos malformados, límites de longitud, y valores nulos antes de que el usuario lo haga.
Ignorar las pruebas cuando fallan bajo presión de deadlines. Una prueba rota que se desactiva "temporalmente" suele volverse permanente y deja un agujero invisible en la cobertura.