En pruebas unitarias intentamos verificar una unidad con la menor cantidad posible de dependencias externas. Pero muchas unidades colaboran con otros objetos, servicios o componentes.
Para controlar esas colaboraciones podemos usar dobles de prueba. Un doble de prueba es un reemplazo controlado de una dependencia real durante una prueba.
En este tema veremos el concepto general, los tipos más conocidos y cuándo conviene usarlos. En los temas siguientes veremos stubs, mocks y fakes con más detalle inicial.
Un doble de prueba es un objeto, función o componente que se usa en una prueba en lugar de una dependencia real.
Sirve para:
Supongamos que una unidad calcula si un usuario recibe una promoción, pero necesita consultar si el usuario está activo.
def puede_recibir_promocion(usuario_id, servicio_usuarios):
usuario = servicio_usuarios.obtener_usuario(usuario_id)
return usuario["activo"] and usuario["puntos"] >= 100
En una prueba unitaria no queremos llamar a un servicio real. Queremos controlar qué usuario devuelve esa dependencia.
Podemos crear un objeto simple que devuelva datos controlados.
class ServicioUsuariosDePrueba:
def obtener_usuario(self, usuario_id):
return {"activo": True, "puntos": 120}
def test_usuario_activo_con_puntos_recibe_promocion():
servicio = ServicioUsuariosDePrueba()
resultado = puede_recibir_promocion(1, servicio)
assert resultado == True
ServicioUsuariosDePrueba reemplaza al servicio real. La prueba controla el usuario devuelto sin depender de una API o base de datos.
Usar dependencias reales en pruebas unitarias puede causar varios problemas:
Un doble ayuda a mantener la prueba enfocada en la unidad.
Existen varios tipos de dobles de prueba. Los nombres pueden variar entre herramientas y equipos, pero la clasificación general es útil.
| Tipo | Propósito principal |
|---|---|
| Dummy | Se pasa como argumento, pero no se usa realmente. |
| Stub | Devuelve respuestas controladas. |
| Fake | Implementación simple que funciona para pruebas. |
| Spy | Registra cómo fue usado para revisarlo después. |
| Mock | Verifica interacciones esperadas. |
Un dummy es un objeto que se pasa porque la firma lo requiere, pero la prueba no lo usa realmente.
def generar_saludo(nombre, logger):
return f"Hola, {nombre}"
def test_generar_saludo():
logger_dummy = None
assert generar_saludo("Ana", logger_dummy) == "Hola, Ana"
En este ejemplo, logger no se usa. El dummy solo permite llamar a la función.
Un stub devuelve una respuesta controlada para la prueba.
class CotizacionStub:
def obtener_cotizacion(self):
return 250
def convertir_a_dolares(monto, servicio_cotizacion):
return monto / servicio_cotizacion.obtener_cotizacion()
def test_convertir_a_dolares_con_cotizacion_controlada():
servicio = CotizacionStub()
assert convertir_a_dolares(1000, servicio) == 4
El stub evita consultar una cotización real y permite usar un valor fijo.
Un fake es una implementación simple que funciona, pero no es la implementación real de producción.
class RepositorioUsuariosFake:
def __init__(self):
self.usuarios = {}
def guardar(self, usuario):
self.usuarios[usuario["id"]] = usuario
def buscar(self, usuario_id):
return self.usuarios.get(usuario_id)
Este fake guarda datos en memoria. Puede servir para pruebas sin usar una base de datos real.
Un spy registra información sobre cómo fue usado. Después la prueba puede inspeccionar esa información.
class NotificadorSpy:
def __init__(self):
self.mensajes = []
def enviar(self, mensaje):
self.mensajes.append(mensaje)
La prueba podría verificar que se intentó enviar cierto mensaje. Esto ayuda cuando el comportamiento importante es una interacción.
Un mock se usa para verificar interacciones esperadas. Por ejemplo, que se llamó a una dependencia con ciertos datos.
La idea general es:
Los mocks son útiles, pero si se usan demasiado pueden acoplar las pruebas a detalles internos. Los veremos de forma básica en un tema posterior.
Conviene usar un doble cuando la dependencia real:
El doble permite controlar el escenario de prueba.
No siempre hace falta un doble. A veces podemos simplificar el diseño y pasar datos directamente.
def calcular_descuento_por_tipo(tipo_cliente):
return 15 if tipo_cliente == "vip" else 0
Esta función no necesita un doble. Podemos probarla con strings simples. Usar mocks aquí solo agregaría complejidad.
Los dobles de prueba son útiles, pero pueden usarse en exceso. Señales de abuso:
El objetivo es aislar lo necesario, no simular todo el sistema.
Para usar dobles con facilidad, el código debe permitir reemplazar dependencias. Esto suele lograrse pasando dependencias como parámetros o mediante inyección de dependencias.
def calcular_precio_en_dolares(monto, servicio_cotizacion):
cotizacion = servicio_cotizacion.obtener_cotizacion()
return monto / cotizacion
La función no crea el servicio real internamente. Lo recibe. Eso permite pasar un stub en la prueba.
Si reemplazamos una dependencia real con un doble, la prueba no verifica la integración con esa dependencia. Eso está bien si nuestro objetivo es unitario.
Pero debemos recordar:
Cada nivel cumple un rol diferente.
| Doble | Uso principal | Ejemplo |
|---|---|---|
| Dummy | Rellenar un parámetro no usado. | logger_dummy = None |
| Stub | Devolver datos controlados. | Cotización fija. |
| Fake | Implementación simple funcional. | Repositorio en memoria. |
| Spy | Registrar llamadas realizadas. | Lista de mensajes enviados. |
| Mock | Verificar interacciones esperadas. | Comprobar que se llamó a enviar correo. |
Conviene elegir el doble más simple que resuelva la necesidad de la prueba.
Usar un mock complejo cuando alcanza un stub simple suele empeorar la prueba.
Antes de usar un doble, revisa:
Los dobles de prueba son herramientas para aislar unidades y controlar dependencias. Permiten escribir pruebas más rápidas, repetibles y enfocadas cuando una unidad colabora con otros componentes.
La clave está en usarlos con criterio. Un doble debe simplificar la prueba y aclarar el escenario, no convertir la preparación en una simulación compleja del sistema.
En el próximo tema veremos stubs, que son dobles usados para reemplazar respuestas controladas.