29. Fakes y objetos simulados simples

29.1 Introducción

Un fake es un doble de prueba que implementa una versión simple de una dependencia real. A diferencia de un stub, que suele devolver respuestas preparadas, un fake puede tener una lógica mínima funcional.

Un ejemplo clásico es un repositorio en memoria que reemplaza temporalmente una base de datos. No es la implementación real de producción, pero permite guardar y consultar datos durante la prueba.

En este tema veremos cómo usar fakes y objetos simulados simples sin convertirlos en sistemas complejos.

29.2 Qué es un fake

Un fake es una implementación alternativa, más simple que la real, pensada para pruebas.

Características habituales:

  • Funciona de manera suficiente para la prueba.
  • No usa recursos externos reales.
  • Suele guardar datos en memoria.
  • Es más rápido y controlable que la dependencia real.
  • Puede comportarse de forma parecida a la dependencia real, pero sin toda su complejidad.
Un fake no solo devuelve un valor fijo: implementa una versión simple y funcional de una dependencia.

29.3 Ejemplo: repositorio real y repositorio fake

Supongamos que una aplicación usa un repositorio de usuarios. El repositorio real podría guardar datos en una base de datos. Para una prueba unitaria, podemos usar una versión en memoria.

class RepositorioUsuariosFake:
    def __init__(self):
        self.usuarios = {}

    def guardar(self, usuario):
        self.usuarios[usuario["id"]] = usuario

    def buscar_por_id(self, usuario_id):
        return self.usuarios.get(usuario_id)

Este fake permite guardar y buscar usuarios sin usar una base de datos real.

29.4 Usar el fake en una prueba

def registrar_usuario(usuario, repositorio):
    repositorio.guardar(usuario)
    return usuario["id"]


def test_registrar_usuario_guarda_usuario_en_repositorio():
    repositorio = RepositorioUsuariosFake()
    usuario = {"id": 1, "nombre": "Ana"}

    usuario_id = registrar_usuario(usuario, repositorio)

    assert usuario_id == 1
    assert repositorio.buscar_por_id(1) == usuario

La prueba verifica que la unidad usa el repositorio y que el usuario queda disponible en la implementación fake.

29.5 Diferencia entre fake y stub

Un stub devuelve respuestas controladas. Un fake tiene una implementación simple que puede responder según operaciones anteriores.

Tipo Ejemplo Característica
Stub Siempre devuelve el mismo usuario. Respuesta fija o preparada.
Fake Guarda usuarios en memoria y luego los busca. Implementación simple funcional.

29.6 Diferencia entre fake y mock

Un mock verifica interacciones esperadas. Un fake ofrece una implementación alternativa simple.

Si solo queremos comprobar que se llamó a guardar, un mock puede servir. Si queremos trabajar con un repositorio simple que permita guardar y buscar datos, un fake puede ser más natural.

La elección depende de qué comportamiento queremos verificar.

29.7 Ejemplo con carrito en memoria

Un fake puede representar un almacenamiento simple de carritos.

class RepositorioCarritosFake:
    def __init__(self):
        self.carritos = {}

    def guardar(self, usuario_id, carrito):
        self.carritos[usuario_id] = carrito

    def obtener(self, usuario_id):
        return self.carritos.get(usuario_id)

Este fake permite probar una unidad que guarda y recupera carritos sin persistencia real.

29.8 Prueba con carrito en memoria

def agregar_item_a_carrito(usuario_id, item, repositorio):
    carrito = repositorio.obtener(usuario_id) or []
    carrito.append(item)
    repositorio.guardar(usuario_id, carrito)
    return carrito


def test_agregar_item_a_carrito_nuevo():
    repositorio = RepositorioCarritosFake()

    carrito = agregar_item_a_carrito(1, "teclado", repositorio)

    assert carrito == ["teclado"]
    assert repositorio.obtener(1) == ["teclado"]

El fake guarda el carrito en memoria. La prueba puede verificar el resultado sin base de datos.

29.9 Cuándo usar un fake

Un fake puede ser útil cuando:

  • La dependencia real es costosa o lenta.
  • Necesitamos una implementación simple con estado.
  • Queremos probar varias operaciones relacionadas.
  • Un stub fijo no alcanza.
  • Una base de datos real sería demasiado pesada para el caso unitario.

El fake debe ser pequeño y específico para la necesidad de la prueba.

29.10 Cuándo evitar un fake

No conviene usar un fake cuando:

  • La dependencia real es simple y rápida.
  • El fake requiere demasiada lógica para comportarse correctamente.
  • Terminamos duplicando la implementación real.
  • Lo que queremos probar es la integración con la dependencia real.
  • El fake se vuelve más difícil de mantener que la prueba.

Un fake complejo puede convertirse en otro sistema que también necesita pruebas.

29.11 Fakes con estado

La principal diferencia práctica de muchos fakes es que conservan estado durante la prueba.

repositorio = RepositorioUsuariosFake()
repositorio.guardar({"id": 1, "nombre": "Ana"})
usuario = repositorio.buscar_por_id(1)

El usuario buscado depende de lo que se guardó antes en el fake. Eso permite simular una dependencia con comportamiento básico.

29.12 Cuidar la independencia

Como los fakes pueden tener estado, debemos crear uno nuevo por prueba para evitar contaminación entre casos.

def test_un_caso():
    repositorio = RepositorioUsuariosFake()
    ...


def test_otro_caso():
    repositorio = RepositorioUsuariosFake()
    ...

No conviene compartir un fake mutable global entre muchas pruebas, porque puede generar dependencia de orden.

29.13 Fakes y reglas de negocio

Un fake no debería contener reglas de negocio complejas. Si el fake empieza a decidir descuentos, permisos o estados de negocio, puede estar haciendo demasiado.

La regla general es:

  • El fake puede simular almacenamiento o una respuesta simple.
  • La unidad probada debe contener la lógica que queremos verificar.
  • El fake no debe repetir la lógica de producción.

29.14 Fake demasiado complejo

Ejemplo riesgoso:

class RepositorioPromocionesFake:
    def obtener_promocion(self, cliente, monto):
        if cliente["tipo"] == "vip" and monto > 10000:
            return {"descuento": 20}
        if cliente["tipo"] == "vip":
            return {"descuento": 15}
        return {"descuento": 0}

Este fake contiene reglas de promoción. Si esas reglas son lo que queremos probar, deberían estar en la unidad productiva y no escondidas en el fake.

29.15 Fake simple y claro

Un fake más claro se limita a almacenar datos preparados.

class RepositorioPromocionesFake:
    def __init__(self, promociones):
        self.promociones = promociones

    def buscar_por_cliente(self, cliente_id):
        return self.promociones.get(cliente_id)

La prueba controla qué promoción existe. La lógica de aplicar promoción queda en la unidad probada.

29.16 Fakes y pruebas de integración

Usar un fake no prueba que la dependencia real funcione. Si reemplazamos una base de datos por un repositorio en memoria, no verificamos consultas SQL, índices, transacciones ni configuración real.

Por eso, una estrategia completa puede incluir:

  • Pruebas unitarias con fake para la lógica.
  • Pruebas de integración con la dependencia real.

Ambas pruebas responden preguntas diferentes.

29.17 Tabla comparativa

Doble Qué hace Ejemplo
Stub Devuelve una respuesta controlada. Cotización fija 250.
Mock Verifica interacción. Confirmar llamada a notificador.
Fake Implementación simple funcional. Repositorio en memoria.
Objeto real simple Se usa directamente. Una clase pequeña sin dependencias externas.

29.18 Nombrar fakes

Conviene nombrar los fakes de forma clara para evitar confusión con clases reales.

  • RepositorioUsuariosFake
  • ServicioCotizacionFake
  • RepositorioCarritosEnMemoria

El nombre debe comunicar que se trata de una implementación de prueba o en memoria.

29.19 Lista de comprobación

Antes de usar un fake, revisa:

  • ¿La dependencia real es demasiado pesada para una prueba unitaria?
  • ¿Necesitas una implementación con estado simple?
  • ¿El fake evita una base de datos, red o archivo real?
  • ¿El fake es pequeño y fácil de entender?
  • ¿No duplica reglas de negocio complejas?
  • ¿Se crea un fake nuevo por prueba?
  • ¿Existe una prueba de integración si la dependencia real también importa?

29.20 Qué debes recordar de este tema

  • Un fake es una implementación simple usada en pruebas.
  • Un repositorio en memoria es un ejemplo común de fake.
  • Los fakes son útiles cuando un stub fijo no alcanza.
  • Un fake puede conservar estado durante una prueba.
  • Debe crearse de forma aislada para evitar dependencia entre pruebas.
  • No debe duplicar reglas complejas de producción.
  • Usar un fake no reemplaza pruebas de integración con la dependencia real.

29.21 Conclusión

Los fakes permiten reemplazar dependencias reales con implementaciones simples y controladas. Son especialmente útiles cuando necesitamos una dependencia con estado, como un repositorio en memoria.

El valor de un fake está en simplificar la prueba sin ocultar la lógica importante. Si el fake se vuelve complejo, probablemente estamos desplazando el problema en lugar de resolverlo.

En el próximo tema veremos inyección de dependencias, una técnica que facilita reemplazar dependencias reales por dobles de prueba.