Un stub es un doble de prueba que reemplaza una dependencia y devuelve respuestas controladas. Se usa cuando queremos probar una unidad sin depender de datos externos, servicios reales o condiciones variables.
La idea es sencilla: si la unidad necesita consultar algo, el stub responde con un valor conocido. Así la prueba puede enfocarse en la lógica de la unidad.
En este tema veremos cómo usar stubs de manera clara y cuándo conviene elegirlos.
Un stub proporciona datos preparados para la prueba. No intenta ser una implementación completa de la dependencia real; solo devuelve lo necesario para el caso.
Sirve para controlar preguntas como:
Supongamos que una función convierte pesos a dólares usando un servicio de cotización.
def convertir_a_dolares(monto, servicio_cotizacion):
cotizacion = servicio_cotizacion.obtener_cotizacion()
return monto / cotizacion
En una prueba unitaria no queremos consultar una cotización real. Queremos usar un valor fijo.
class CotizacionStub:
def obtener_cotizacion(self):
return 250
def test_convertir_a_dolares_con_cotizacion_controlada():
servicio = CotizacionStub()
resultado = convertir_a_dolares(1000, servicio)
assert resultado == 4
El stub devuelve siempre 250. La prueba puede verificar la fórmula sin depender de una API, base de datos o valor cambiante.
En el ejemplo anterior, la prueba no verifica si la cotización real es correcta. Verifica que la unidad use la cotización recibida para calcular el resultado.
Esta distinción es importante:
El stub nos permite separar esas responsabilidades.
Ahora supongamos que una regla de promoción depende de datos de usuario obtenidos desde un servicio.
def puede_recibir_promocion(usuario_id, servicio_usuarios):
usuario = servicio_usuarios.obtener_usuario(usuario_id)
return usuario["activo"] and usuario["puntos"] >= 100
Podemos usar stubs para simular distintos usuarios sin consultar una base de datos real.
class UsuarioActivoStub:
def obtener_usuario(self, usuario_id):
return {"activo": True, "puntos": 120}
def test_usuario_activo_con_puntos_recibe_promocion():
servicio = UsuarioActivoStub()
resultado = puede_recibir_promocion(1, servicio)
assert resultado == True
El stub devuelve un usuario activo con suficientes puntos. La prueba verifica el caso positivo de la regla.
class UsuarioInactivoStub:
def obtener_usuario(self, usuario_id):
return {"activo": False, "puntos": 200}
def test_usuario_inactivo_no_recibe_promocion():
servicio = UsuarioInactivoStub()
resultado = puede_recibir_promocion(1, servicio)
assert resultado == False
Este stub permite probar otra clase de comportamiento: aunque tenga puntos, el usuario inactivo no recibe promoción.
Si crear una clase por caso genera repetición, podemos construir un stub configurable.
class ServicioUsuariosStub:
def __init__(self, usuario):
self.usuario = usuario
def obtener_usuario(self, usuario_id):
return self.usuario
def test_usuario_sin_puntos_suficientes_no_recibe_promocion():
usuario = {"activo": True, "puntos": 50}
servicio = ServicioUsuariosStub(usuario)
resultado = puede_recibir_promocion(1, servicio)
assert resultado == False
El stub sigue siendo simple, pero ahora podemos controlar la respuesta desde cada prueba.
Un stub debe hacer la prueba más clara, no más confusa. Si la preparación del stub ocupa demasiado espacio, puede ocultar el comportamiento que realmente queremos verificar.
Buenas señales:
Un stub también puede devolver una respuesta que represente una situación de error controlado.
class UsuarioNoEncontradoStub:
def obtener_usuario(self, usuario_id):
return None
def puede_recibir_promocion(usuario_id, servicio_usuarios):
usuario = servicio_usuarios.obtener_usuario(usuario_id)
if usuario is None:
return False
return usuario["activo"] and usuario["puntos"] >= 100
def test_usuario_inexistente_no_recibe_promocion():
servicio = UsuarioNoEncontradoStub()
assert puede_recibir_promocion(1, servicio) == False
El stub permite probar cómo reacciona la unidad cuando la dependencia no encuentra datos.
Si una unidad depende de configuración, podemos usar un stub para controlar esa configuración.
class ConfiguracionStub:
def obtener_moneda(self):
return "USD"
def mostrar_precio(precio, configuracion):
return f"{configuracion.obtener_moneda()} {precio}"
def test_mostrar_precio_en_dolares():
configuracion = ConfiguracionStub()
assert mostrar_precio(100, configuracion) == "USD 100"
La prueba no depende de variables de entorno ni archivos de configuración reales.
Un stub se centra en devolver datos. Un mock se centra en verificar interacciones. Aunque algunas herramientas mezclan estos conceptos, la diferencia conceptual ayuda.
| Tipo | Pregunta que responde |
|---|---|
| Stub | ¿Qué respuesta controlada recibe la unidad? |
| Mock | ¿La unidad llamó a la dependencia como se esperaba? |
Si solo necesitamos controlar un dato de entrada indirecto, un stub suele ser suficiente.
Los stubs son útiles cuando:
No conviene usar stubs cuando:
Un stub debe reducir complejidad, no aumentarla.
Un stub no debería convertirse en una segunda implementación del sistema real.
class ServicioDescuentosStubComplejo:
def obtener_descuento(self, tipo, monto):
if tipo == "vip" and monto > 10000:
return 20
if tipo == "vip":
return 15
if tipo == "empleado":
return 25
return 0
Este stub tiene demasiada lógica. Si necesitamos tanta lógica, quizá estamos probando el lugar equivocado o conviene separar reglas.
| Dependencia | Respuesta del stub | Comportamiento probado |
|---|---|---|
| Servicio de cotización | Cotización 250 | Conversión de moneda. |
| Servicio de usuarios | Usuario activo con 120 puntos | Promoción disponible. |
| Servicio de usuarios | Usuario inactivo | Promoción rechazada. |
| Configuración | Moneda USD | Formato de precio. |
| Búsqueda | Resultado inexistente | Manejo de ausencia de datos. |
El nombre de la prueba debe expresar la condición representada por el stub.
def test_usuario_inactivo_no_recibe_promocion():
servicio = ServicioUsuariosStub({"activo": False, "puntos": 200})
resultado = puede_recibir_promocion(1, servicio)
assert resultado == False
El nombre no habla del stub; habla del comportamiento: usuario inactivo no recibe promoción. Eso mantiene la prueba orientada al resultado.
Al usar un stub, revisa:
Los stubs son una forma simple y efectiva de controlar respuestas de dependencias en pruebas unitarias. Permiten probar la lógica de una unidad sin depender de servicios reales, datos cambiantes o recursos lentos.
La clave está en mantenerlos pequeños y orientados al caso. Un buen stub da exactamente la respuesta que la prueba necesita y nada más.
En el próximo tema veremos mocks, que se usan para verificar interacciones básicas.