Las pruebas unitarias son solo un nivel dentro de una estrategia de testing. Para entender bien su utilidad, también debemos distinguirlas de las pruebas de integración y de las pruebas end-to-end.
Estos tres niveles no compiten entre sí. Cada uno responde preguntas diferentes, detecta tipos de problemas distintos y tiene costos diferentes de ejecución y mantenimiento.
En este tema nos concentraremos en comparar los niveles para saber qué conviene probar como unidad, qué conviene probar como integración y qué conviene dejar para una prueba de flujo completo.
Una forma simple de diferenciarlos es observar la pregunta principal que intenta responder cada prueba:
| Nivel | Pregunta principal |
|---|---|
| Prueba unitaria | ¿Esta unidad de código se comporta correctamente? |
| Prueba de integración | ¿Estas partes del sistema colaboran correctamente? |
| Prueba end-to-end | ¿El usuario puede completar un flujo real de principio a fin? |
La diferencia central está en el alcance. Cuanto más alto es el nivel, más partes del sistema participan en la prueba.
Una prueba unitaria verifica una unidad pequeña de código: una función, método, clase o módulo con una responsabilidad clara. Su objetivo es comprobar lógica específica con datos controlados.
Ejemplo: verificar que una función calcule correctamente el descuento de una compra.
def calcular_descuento(monto):
if monto > 10000:
return monto * 0.15
return 0
def test_compra_mayor_a_10000_recibe_descuento():
assert calcular_descuento(12000) == 1800
Esta prueba no usa base de datos, navegador, servidor web ni interfaz de usuario. Solo verifica una regla de cálculo.
Una prueba de integración verifica que dos o más partes del sistema trabajen correctamente juntas. Su foco no está en una unidad aislada, sino en la colaboración entre componentes.
Ejemplos de integración:
En estas pruebas pueden aparecer recursos reales o simulados, como base de datos de prueba, servidor local o componentes conectados entre sí.
Una prueba end-to-end, también llamada E2E, verifica un flujo completo desde la perspectiva del usuario o de un cliente externo del sistema.
Por ejemplo, en una tienda en línea, una prueba end-to-end podría hacer lo siguiente:
Este tipo de prueba da confianza sobre el flujo completo, pero suele ser más lenta, más costosa y más sensible a cambios de entorno o interfaz.
| Característica | Unitarias | Integración | End-to-end |
|---|---|---|---|
| Alcance | Una unidad pequeña. | Varias partes conectadas. | Flujo completo. |
| Velocidad | Muy rápidas. | Intermedia. | Más lentas. |
| Dependencias | Mínimas. | Algunas dependencias reales o controladas. | Muchas partes del sistema. |
| Diagnóstico de fallas | Más directo. | Intermedio. | Más amplio y complejo. |
| Costo de mantenimiento | Bajo si están bien diseñadas. | Medio. | Alto si se abusa de ellas. |
Supongamos una funcionalidad de registro de usuarios. La regla dice que una persona debe tener al menos 18 años para registrarse.
Podemos probar esa funcionalidad en distintos niveles:
| Nivel | Qué probaría |
|---|---|
| Unitaria | La función puede_registrarse(edad) devuelve True para 18 y False para 17. |
| Integración | El servicio de registro usa la validación y guarda al usuario válido en la base de datos de prueba. |
| End-to-end | Un usuario completa el formulario en el navegador y ve el resultado correspondiente. |
La misma regla puede participar en varios niveles, pero cada nivel mira un aspecto diferente del sistema.
La parte más pequeña de la regla puede probarse así:
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
Estas pruebas son rápidas y directas. Si fallan, sabemos que el problema está en la regla de edad o en la expectativa de la prueba.
Una prueba de integración podría verificar que un servicio usa correctamente esa regla antes de guardar datos:
def test_servicio_registra_usuario_mayor_de_edad(base_de_datos_de_prueba):
servicio = ServicioRegistro(base_de_datos_de_prueba)
servicio.registrar(nombre="Ana", edad=18)
usuario = base_de_datos_de_prueba.buscar_usuario("Ana")
assert usuario is not None
Aquí ya no estamos probando solo la función de edad. También participa el servicio de registro y una base de datos de prueba. Si falla, puede haber un problema en la validación, en el servicio, en el mapeo de datos o en la persistencia.
Una prueba end-to-end podría simular el flujo completo en la interfaz:
def test_usuario_mayor_de_edad_puede_registrarse_en_la_web(navegador):
navegador.abrir("/registro")
navegador.escribir("#nombre", "Ana")
navegador.escribir("#edad", "18")
navegador.click("#boton-registrar")
assert navegador.contiene_texto("Registro completado")
Este ejemplo es conceptual. Lo importante es notar que ahora participan la interfaz, el servidor, la validación, la persistencia y la respuesta visible para el usuario.
| Tipo de defecto | Nivel más adecuado |
|---|---|
| Cálculo incorrecto en una función. | Prueba unitaria. |
| Una clase no guarda correctamente en la base de datos. | Prueba de integración. |
| Un formulario no permite completar el flujo de compra. | Prueba end-to-end. |
| Un servicio llama con parámetros incorrectos a otro módulo. | Prueba de integración. |
| Una regla de negocio devuelve una decisión incorrecta. | Prueba unitaria. |
| El usuario no ve el mensaje esperado después de una operación. | Prueba end-to-end o de interfaz. |
Las pruebas unitarias suelen ejecutarse en milisegundos o segundos porque no levantan toda la aplicación. Esto permite correrlas con frecuencia mientras desarrollamos.
Las pruebas de integración suelen tardar más porque conectan varias partes. Pueden requerir preparar una base de datos, iniciar servicios o cargar configuraciones.
Las pruebas end-to-end suelen ser las más lentas porque recorren flujos completos y dependen de más elementos. Además, cuando fallan, el diagnóstico puede requerir revisar varias capas del sistema.
La pirámide de testing es una idea usada para representar una estrategia equilibrada de pruebas automatizadas. En general, propone tener muchas pruebas unitarias, una cantidad menor de pruebas de integración y menos pruebas end-to-end.
La razón es práctica: las pruebas unitarias son rápidas y baratas de ejecutar; las de integración aportan confianza sobre colaboraciones importantes; las end-to-end verifican flujos críticos pero son más costosas.
No debe interpretarse como una regla matemática exacta. Es una guía para evitar depender únicamente de pruebas grandes y lentas.
Conviene elegir una prueba unitaria cuando queremos verificar:
Si podemos comprobar el comportamiento sin base de datos, navegador ni red, probablemente estamos ante un buen candidato para prueba unitaria.
Conviene una prueba de integración cuando el riesgo principal está en la colaboración entre partes. Por ejemplo:
Si las unidades funcionan por separado pero no sabemos si colaboran bien, necesitamos una prueba de integración.
Conviene una prueba end-to-end cuando queremos comprobar un flujo crítico tal como lo usaría una persona o un sistema externo.
Ejemplos:
Estas pruebas son valiosas, pero conviene reservarlas para flujos importantes. Si intentamos verificar cada pequeña regla con pruebas end-to-end, la suite puede volverse lenta y difícil de mantener.
Cuando un equipo comienza a automatizar, a veces intenta probar casi todo desde la interfaz o desde flujos completos. Esto parece atractivo porque se parece al uso real del sistema, pero suele traer problemas.
Si una regla simple falla, una prueba end-to-end puede tardar más en ejecutarse y dar menos información sobre la causa. Además, puede fallar por elementos ajenos a la regla: selectores de interfaz, datos de prueba, sesión, red, tiempos de espera o configuración.
La regla general es elegir el nivel más bajo que permita verificar el comportamiento con claridad. Si una regla puede probarse unitariamente, no hace falta depender siempre de un flujo completo para cubrirla.
El extremo opuesto también es riesgoso. Tener muchas pruebas unitarias no garantiza que la aplicación funcione correctamente cuando sus partes se conectan.
Dos unidades pueden estar bien probadas de manera aislada y aun así fallar al integrarse. Por ejemplo, una función puede devolver una fecha en un formato y otra esperar un formato diferente. Cada una puede pasar sus pruebas unitarias, pero la colaboración puede fallar.
Por eso necesitamos una combinación equilibrada. Las pruebas unitarias son una base importante, pero no reemplazan las pruebas de integración ni las end-to-end.
Antes de escribir una prueba, conviene hacer estas preguntas:
Estas preguntas ayudan a ubicar la prueba en el nivel correcto y evitan suites lentas o con pruebas redundantes.
Las pruebas unitarias, de integración y end-to-end cumplen funciones distintas. Una estrategia de testing sólida usa cada nivel para lo que mejor sabe comprobar.
En este curso profundizaremos en pruebas unitarias, pero siempre teniendo presente su lugar dentro de una estrategia más amplia. Saber qué no corresponde a una prueba unitaria es tan importante como saber escribirla bien.
En el próximo tema estudiaremos los beneficios y límites de las pruebas unitarias para tener expectativas realistas sobre su valor.