26. Dobles de prueba: concepto general

26.1 Introducción

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.

26.2 Qué es un doble de prueba

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:

  • Evitar llamadas a servicios externos.
  • Controlar respuestas de una dependencia.
  • Simular situaciones difíciles de reproducir.
  • Hacer la prueba más rápida.
  • Verificar cómo una unidad colabora con otra.
Un doble de prueba reemplaza una dependencia real para que la prueba pueda controlar mejor el escenario.

26.3 Ejemplo del mundo real

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.

26.4 Reemplazar la 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.

26.5 Por qué no usar siempre la dependencia real

Usar dependencias reales en pruebas unitarias puede causar varios problemas:

  • La prueba se vuelve lenta.
  • El resultado depende de datos externos.
  • La dependencia puede estar caída.
  • Se requieren credenciales o configuración.
  • Puede haber efectos reales no deseados, como enviar correos.
  • El diagnóstico se vuelve más difícil.

Un doble ayuda a mantener la prueba enfocada en la unidad.

26.6 Tipos principales de dobles

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.

26.7 Dummy

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.

26.8 Stub

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.

26.9 Fake

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.

26.10 Spy

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.

26.11 Mock

Un mock se usa para verificar interacciones esperadas. Por ejemplo, que se llamó a una dependencia con ciertos datos.

La idea general es:

  • Preparar el mock con una expectativa.
  • Ejecutar la unidad.
  • Verificar que la interacción ocurrió como se esperaba.

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.

26.12 Cuándo usar un doble

Conviene usar un doble cuando la dependencia real:

  • Es lenta.
  • No es determinística.
  • Tiene efectos reales no deseados.
  • Es difícil de configurar.
  • Depende de red, base de datos o archivos.
  • Debe devolver una situación específica difícil de provocar.

El doble permite controlar el escenario de prueba.

26.13 Cuándo no usar un doble

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.

26.14 Riesgo de abuso

Los dobles de prueba son útiles, pero pueden usarse en exceso. Señales de abuso:

  • La prueba configura muchos mocks para verificar una regla simple.
  • La prueba sabe demasiado sobre pasos internos.
  • Un refactor interno rompe muchas pruebas aunque el comportamiento no cambió.
  • La preparación de la prueba es más compleja que el código probado.
  • Se reemplazan objetos que podrían usarse directamente sin costo.

El objetivo es aislar lo necesario, no simular todo el sistema.

26.15 Dobles y diseño

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.

26.16 Dobles y nivel de 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:

  • La prueba unitaria verifica la lógica de la unidad.
  • Una prueba de integración debe verificar que la dependencia real colabore correctamente.
  • Un doble no reemplaza todas las pruebas de integración.

Cada nivel cumple un rol diferente.

26.17 Tabla comparativa

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.

26.18 Elegir el doble más simple

Conviene elegir el doble más simple que resuelva la necesidad de la prueba.

  • Si solo necesitamos un valor de retorno, un stub suele alcanzar.
  • Si necesitamos guardar y consultar datos simples, un fake puede servir.
  • Si necesitamos saber si una llamada ocurrió, un spy o mock puede ser útil.
  • Si la dependencia no se usa, un dummy puede bastar.

Usar un mock complejo cuando alcanza un stub simple suele empeorar la prueba.

26.19 Lista de comprobación

Antes de usar un doble, revisa:

  • ¿Qué dependencia quiero reemplazar?
  • ¿Por qué no conviene usar la dependencia real?
  • ¿Necesito controlar una respuesta o verificar una interacción?
  • ¿Puedo simplificar el diseño pasando datos directamente?
  • ¿El doble elegido es el más simple posible?
  • ¿La prueba seguirá verificando comportamiento observable?
  • ¿Necesito una prueba de integración complementaria?

26.20 Qué debes recordar de este tema

  • Un doble de prueba reemplaza una dependencia real durante una prueba.
  • Sirve para controlar escenarios y evitar dependencias lentas o variables.
  • Tipos comunes: dummy, stub, fake, spy y mock.
  • No todos los dobles tienen el mismo propósito.
  • Conviene elegir el doble más simple que resuelva el caso.
  • Abusar de dobles puede acoplar pruebas a detalles internos.
  • Los dobles no reemplazan las pruebas de integración necesarias.

26.21 Conclusión

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.