Las pruebas unitarias son una herramienta muy valiosa, pero no son una solución mágica. Entender sus beneficios y sus límites permite usarlas con criterio y evitar expectativas equivocadas.
Cuando se aplican bien, ayudan a detectar errores temprano, protegen comportamientos importantes y dan confianza al modificar código. Cuando se aplican mal, pueden convertirse en una carga: pruebas frágiles, difíciles de leer o que fallan por motivos poco relevantes.
En este tema veremos qué aportan realmente las pruebas unitarias, qué problemas no resuelven y cómo reconocer cuándo están siendo útiles.
El beneficio más visible de las pruebas unitarias es la retroalimentación rápida. Una buena suite unitaria puede ejecutarse en pocos segundos y avisar si una regla dejó de funcionar.
Esto cambia la forma de trabajar. En lugar de esperar a probar manualmente toda la aplicación, podemos verificar muchas unidades pequeñas durante el desarrollo.
La rapidez no es un detalle menor. Si una prueba tarda demasiado, se ejecuta menos. Si se ejecuta menos, detecta problemas más tarde.
Una prueba unitaria falla cerca del código que verifica. Si una función de cálculo tiene un error, una prueba unitaria de esa función puede mostrarlo directamente.
def calcular_iva(precio):
return precio * 0.20
def test_calcular_iva_del_21_por_ciento():
assert calcular_iva(1000) == 210
En este ejemplo, la prueba falla porque la función calcula 20% en lugar de 21%. El diagnóstico es directo: el problema está en la regla de cálculo.
Si ese mismo error se detectara recién en una pantalla de facturación completa, tendríamos que revisar más capas antes de llegar a la causa.
Una regresión ocurre cuando algo que funcionaba deja de funcionar después de un cambio. Las pruebas unitarias ayudan a evitar que comportamientos importantes se rompan sin aviso.
Por ejemplo, si una regla de descuento ya fue implementada y probada, sus pruebas quedan como protección para futuras modificaciones.
def aplicar_descuento(precio, porcentaje):
return precio - (precio * porcentaje / 100)
def test_descuento_cero_no_cambia_el_precio():
assert aplicar_descuento(800, 0) == 800
def test_descuento_del_25_por_ciento():
assert aplicar_descuento(800, 25) == 600
Si alguien cambia la función y rompe uno de esos casos, la prueba lo detecta. Ese es uno de los usos más prácticos de una suite unitaria.
Refactorizar significa mejorar la estructura interna del código sin cambiar su comportamiento observable. Es una actividad necesaria para mantener un proyecto saludable, pero puede ser riesgosa si no tenemos pruebas.
Las pruebas unitarias dan una base de confianza: antes de refactorizar, ejecutamos las pruebas; después del cambio, las ejecutamos otra vez. Si siguen pasando, tenemos evidencia de que los comportamientos cubiertos se conservaron.
Esto no garantiza que todo el sistema esté perfecto, pero reduce mucho el riesgo al modificar código existente.
Una prueba unitaria bien nombrada y bien escrita funciona como un ejemplo ejecutable del comportamiento esperado.
def puede_votar(edad):
return edad >= 16
def test_persona_de_16_anios_puede_votar():
assert puede_votar(16) == True
def test_persona_de_15_anios_no_puede_votar():
assert puede_votar(15) == False
Estas pruebas comunican la regla con ejemplos concretos. Una persona que lee el código entiende que el límite está en 16 años. Además, la documentación se ejecuta: si la regla cambia o se rompe, las pruebas lo muestran.
Las pruebas unitarias suelen empujar hacia un diseño más claro. Para probar una unidad con facilidad, necesitamos que tenga entradas controlables, salidas observables y dependencias razonables.
Si una función es difícil de probar porque hace demasiadas cosas, esa dificultad puede revelar un problema de diseño.
Comparación simple:
| Código difícil de probar | Código más testeable |
|---|---|
| Mezcla cálculo, base de datos y envío de correo. | Separa cálculo, persistencia y notificación. |
| Depende directamente de la fecha actual del sistema. | Recibe la fecha como parámetro o dependencia controlada. |
| Usa variables globales modificables. | Trabaja con datos explícitos. |
En un equipo, varias personas modifican código al mismo tiempo. Las pruebas unitarias ayudan a detectar si un cambio rompe una regla que otra persona esperaba conservar.
También sirven como comunicación técnica. Una prueba con nombre claro explica una expectativa sin depender de una conversación informal.
Por ejemplo, una prueba llamada test_cliente_inactivo_no_puede_acceder_a_promocion comunica una regla de negocio concreta. Si alguien cambia esa regla, deberá actualizar la prueba o discutir si el comportamiento esperado cambió.
Muchas verificaciones pequeñas no necesitan repetirse manualmente una y otra vez. Si una regla puede comprobarse directamente con una prueba unitaria, conviene automatizarla.
Esto libera tiempo para pruebas manuales de mayor valor, como exploración, revisión de experiencia de usuario o análisis de escenarios nuevos.
Automatizar reglas unitarias no elimina la necesidad de probar la aplicación completa, pero evita usar la interfaz como única forma de verificar cada detalle interno.
| Beneficio | Qué aporta |
|---|---|
| Rapidez | Permite ejecutar pruebas con frecuencia. |
| Diagnóstico | Ayuda a localizar errores cerca de su origen. |
| Regresión | Protege comportamientos ya implementados. |
| Refactoring | Da confianza al mejorar la estructura del código. |
| Documentación | Expresa reglas mediante ejemplos ejecutables. |
| Diseño | Favorece unidades con responsabilidades claras. |
Una prueba unitaria verifica una unidad. Por definición, no comprueba que toda la aplicación funcione de principio a fin.
Una función puede calcular correctamente un total, pero la pantalla podría mostrarlo mal. Una validación puede funcionar, pero el formulario podría no llamarla. Un servicio puede devolver el resultado correcto, pero la base de datos podría guardar otro formato.
Por eso las pruebas unitarias deben complementarse con pruebas de integración, de interfaz, end-to-end y otras según el riesgo del sistema.
Dos unidades pueden funcionar perfectamente por separado y fallar cuando trabajan juntas. Esto ocurre cuando hay diferencias en formatos, contratos, configuraciones o expectativas.
Ejemplo conceptual:
DD/MM/AAAA.AAAA-MM-DD.Este tipo de problema requiere pruebas de integración. Las unitarias ayudan, pero no alcanzan para verificar contratos entre componentes.
Las pruebas unitarias no pueden decir si una pantalla es clara, si el mensaje visible ayuda al usuario, si el flujo es cómodo o si el diseño es accesible.
Una función puede devolver el mensaje correcto y aun así la interfaz puede mostrarlo en un lugar poco visible, con mal contraste o en un momento inoportuno.
Para esos aspectos se necesitan pruebas de interfaz, revisiones de usabilidad, accesibilidad, pruebas exploratorias o pruebas con usuarios según el contexto.
Ningún conjunto de pruebas puede demostrar que un programa no tiene errores. Las pruebas unitarias verifican los casos que escribimos, no todos los casos posibles.
Si no pensamos en un caso borde, la suite puede pasar aunque el defecto exista. Por ejemplo, una función de división puede estar probada con valores normales y fallar con divisor cero si nadie escribió ese caso.
def dividir(a, b):
return a / b
def test_dividir_10_por_2():
assert dividir(10, 2) == 5
Esta prueba es correcta para el caso elegido, pero no dice nada sobre qué ocurre cuando b vale cero. Las pruebas son tan buenas como los casos que seleccionamos.
Una suite con muchas pruebas puede parecer tranquilizadora, pero si esas pruebas verifican poco o están mal diseñadas, pueden dar una falsa sensación de seguridad.
Ejemplo de prueba débil:
def test_calculo():
calcular_total(1000, 21)
La prueba ejecuta código, pero no verifica el resultado. Puede pasar aunque el cálculo sea incorrecto.
Una prueba útil expresa una expectativa:
def test_calcular_total_con_impuesto_del_21_por_ciento():
assert calcular_total(1000, 21) == 1210
La cantidad de pruebas importa menos que la calidad de las verificaciones.
Las pruebas unitarias también son código. Deben leerse, ejecutarse, actualizarse y mantenerse. Si están mal diseñadas, pueden dificultar los cambios en lugar de ayudarlos.
Las pruebas se vuelven costosas cuando:
El objetivo no es tener pruebas a cualquier precio, sino pruebas que aporten más valor del costo que generan.
Una prueba unitaria suele aportar valor cuando cumple varias de estas condiciones:
Una prueba así se convierte en una inversión técnica. Su valor aparece cada vez que alguien modifica el código y necesita confianza.
No todo merece una prueba unitaria aislada. Puede no convenir cuando:
Esto no significa ignorar riesgos. Significa elegir el nivel de prueba adecuado y evitar pruebas unitarias que no aportan información útil.
| Las pruebas unitarias ayudan a... | Pero no reemplazan... |
|---|---|
| Verificar reglas pequeñas. | Pruebas de integración entre componentes. |
| Detectar errores temprano. | Pruebas de flujos completos. |
| Dar confianza al refactorizar. | Revisión de requisitos y validación del negocio. |
| Documentar ejemplos de comportamiento. | Documentación funcional cuando sea necesaria. |
| Reducir verificaciones manuales repetitivas. | Pruebas exploratorias y revisión de experiencia de usuario. |
Las pruebas unitarias son más útiles cuando se entienden como una herramienta de retroalimentación rápida, protección de comportamiento y mejora del diseño. Su valor no está en existir, sino en verificar reglas importantes de manera clara y mantenible.
Al mismo tiempo, tienen límites. No aseguran que toda la aplicación funcione, no validan la experiencia de usuario y no reemplazan otros niveles de prueba.
En el próximo tema veremos cuándo conviene escribir pruebas unitarias, para decidir mejor en qué situaciones aportan más valor.