27. Stubs: reemplazar respuestas controladas

27.1 Introducción

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.

27.2 Qué hace un stub

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:

  • ¿Qué usuario devuelve el servicio?
  • ¿Qué cotización devuelve la API?
  • ¿Qué configuración está activa?
  • ¿Qué permisos tiene el usuario?
  • ¿Qué respuesta devuelve una dependencia ante cierto dato?
Un stub responde de forma controlada. Su objetivo principal es dar datos a la unidad probada.

27.3 Ejemplo con cotización

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.

27.4 Stub para cotización

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.

27.5 Qué se está probando

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:

  • La prueba unitaria verifica la lógica de conversión.
  • Otra prueba, de integración, puede verificar la comunicación con el servicio real de cotización.

El stub nos permite separar esas responsabilidades.

27.6 Ejemplo con usuario

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.

27.7 Stub para usuario activo

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.

27.8 Stub para usuario inactivo

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.

27.9 Stub parametrizable

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.

27.10 Stubs y claridad

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:

  • El stub devuelve pocos datos relevantes.
  • El caso probado se entiende al leer la prueba.
  • La respuesta controlada está cerca de la aserción.
  • No se simula comportamiento innecesario.

27.11 Stubs y errores esperados

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.

27.12 Stubs para configuración

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.

27.13 Diferencia entre stub y mock

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.

27.14 Cuándo usar stubs

Los stubs son útiles cuando:

  • La dependencia devuelve datos que la unidad necesita.
  • Queremos evitar una API, base de datos o archivo real.
  • Necesitamos simular una respuesta específica.
  • La respuesta real es lenta o variable.
  • Queremos probar casos difíciles de reproducir con la dependencia real.

27.15 Cuándo no usar stubs

No conviene usar stubs cuando:

  • La unidad puede recibir el dato directamente como parámetro simple.
  • La dependencia real es un objeto simple y barato de crear.
  • El stub requiere reproducir demasiada lógica.
  • La prueba queda más compleja que el código probado.
  • Lo que queremos verificar es la integración real con la dependencia.

Un stub debe reducir complejidad, no aumentarla.

27.16 Evitar stubs con lógica compleja

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.

27.17 Tabla de ejemplos

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.

27.18 Stubs y nombres de pruebas

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.

27.19 Lista de comprobación

Al usar un stub, revisa:

  • ¿Qué dependencia reemplaza?
  • ¿Qué respuesta controlada devuelve?
  • ¿Esa respuesta es relevante para el caso?
  • ¿El stub es más simple que la dependencia real?
  • ¿La prueba sigue verificando comportamiento de la unidad?
  • ¿El stub evita una dependencia lenta o variable?
  • ¿No está duplicando lógica compleja?

27.20 Qué debes recordar de este tema

  • Un stub es un doble que devuelve respuestas controladas.
  • Sirve para aislar la unidad de dependencias reales.
  • Es útil cuando necesitamos datos conocidos para probar una regla.
  • Un stub debe ser simple y fácil de entender.
  • No debe duplicar lógica compleja de producción.
  • Si solo necesitamos un dato simple, quizá podemos pasarlo directamente.
  • Un stub no prueba la integración real con la dependencia.

27.21 Conclusión

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.