1. Qué son los dobles de prueba y por qué se usan en testing

1.1 Objetivo del tema

En este primer tema veremos qué es un doble de prueba y por qué resulta útil cuando una unidad de código depende de otro componente. El objetivo no es memorizar nombres, sino entender el problema práctico que resuelven los stubs, mocks, fakes, spies y dummies.

Trabajaremos con ejemplos simples en Python usando pytest. Primero veremos el código sin doble de prueba, luego identificaremos el problema y finalmente escribiremos un stub manual para controlar la dependencia.

Objetivo práctico: probar una función Python sin depender de una API real, una base de datos, una fecha del sistema o cualquier recurso externo difícil de controlar.

1.2 El problema de las dependencias reales

Una prueba unitaria debería verificar una porción pequeña de comportamiento. Sin embargo, en los proyectos reales muchas funciones no trabajan solas: consultan servicios externos, leen archivos, piden datos a una base de datos, usan la hora actual o invocan otros objetos.

Cuando una prueba usa esas dependencias reales, puede volverse lenta, frágil o impredecible. También puede fallar por motivos que no pertenecen a la unidad que queremos probar.

  • Una API externa puede estar caída o responder lento.
  • Una base de datos puede no tener los datos esperados.
  • Un archivo puede no existir en el entorno de pruebas.
  • La fecha actual puede cambiar el resultado de la prueba.
  • Un servicio de correo puede enviar mensajes reales por accidente.

1.3 Qué es un doble de prueba

Un doble de prueba es un reemplazo controlado de una dependencia real. Su nombre viene de la idea de un "doble" en una escena: ocupa el lugar de otro componente durante la prueba, pero no necesariamente implementa todo el comportamiento real.

Usamos un doble de prueba cuando queremos que la prueba sea rápida, determinística y enfocada en el comportamiento que estamos verificando.

Un doble de prueba no existe para ocultar errores. Existe para aislar una prueba y controlar con precisión las condiciones del escenario.

1.4 Un ejemplo sin dobles de prueba

Supongamos que tenemos una función que calcula si un cliente puede recibir envío gratis. Para decidirlo, consulta el total comprado por el cliente mediante un objeto externo:

def tiene_envio_gratis(cliente_id, servicio_compras):
    total = servicio_compras.obtener_total_comprado(cliente_id)
    return total >= 50000

La lógica que queremos probar es pequeña: si el total comprado es mayor o igual a 50000, la función debe devolver True. Si es menor, debe devolver False.

El problema es que servicio_compras podría consultar una base de datos, una API o un sistema externo. Para probar esta función no necesitamos que esa dependencia real funcione. Solo necesitamos controlar qué total devuelve.

1.5 Crear un stub manual

Un stub es un doble de prueba que devuelve datos preparados para la prueba. En este caso podemos crear una clase simple que tenga el método esperado:

class ServicioComprasStub:
    def obtener_total_comprado(self, cliente_id):
        return 65000

Este stub no consulta ninguna base de datos. Siempre devuelve 65000, que es exactamente el dato que necesitamos para probar el caso de envío gratis.

1.6 Primera prueba con el stub

Ahora usamos el stub dentro de una prueba con pytest:

from promociones import tiene_envio_gratis


class ServicioComprasStub:
    def obtener_total_comprado(self, cliente_id):
        return 65000


def test_cliente_con_total_suficiente_tiene_envio_gratis():
    servicio = ServicioComprasStub()

    resultado = tiene_envio_gratis("CLI-100", servicio)

    assert resultado is True

La prueba es rápida y predecible. No importa si la base de datos real existe o no. Tampoco importa si el cliente CLI-100 tiene compras reales. El escenario está completamente controlado por la prueba.

1.7 Probar también el caso negativo

Para probar el caso donde el cliente no alcanza el mínimo, podemos crear otro stub:

class ServicioComprasSinMinimoStub:
    def obtener_total_comprado(self, cliente_id):
        return 12000


def test_cliente_con_total_insuficiente_no_tiene_envio_gratis():
    servicio = ServicioComprasSinMinimoStub()

    resultado = tiene_envio_gratis("CLI-200", servicio)

    assert resultado is False

En ambos casos estamos probando la misma función, pero con respuestas diferentes de la dependencia. Esa es una de las ventajas principales de los dobles de prueba: nos permiten construir escenarios específicos sin preparar un sistema externo completo.

1.8 Mejorar el stub para reutilizarlo

Si necesitamos varios totales distintos, podemos hacer un stub configurable:

class ServicioComprasStub:
    def __init__(self, total):
        self.total = total

    def obtener_total_comprado(self, cliente_id):
        return self.total


def test_cliente_con_total_suficiente_tiene_envio_gratis():
    servicio = ServicioComprasStub(total=65000)

    assert tiene_envio_gratis("CLI-100", servicio) is True


def test_cliente_con_total_insuficiente_no_tiene_envio_gratis():
    servicio = ServicioComprasStub(total=12000)

    assert tiene_envio_gratis("CLI-200", servicio) is False

Este diseño deja claro qué dato controla cada prueba. Además, evita duplicar clases de stub para cada escenario.

1.9 Qué estamos probando realmente

En el ejemplo anterior no estamos probando si servicio_compras funciona correctamente. Eso corresponde a otra prueba. Estamos probando que tiene_envio_gratis interpreta correctamente el total recibido.

Esta separación es importante. Una prueba con dobles debe tener un objetivo claro: aislar una unidad, controlar sus colaboraciones y verificar su comportamiento observable.

Si una prueba falla, queremos que la causa esté cerca del código que la prueba intenta verificar.

1.10 Tipos frecuentes de dobles de prueba

En testing se usan varios tipos de dobles. A lo largo del curso los veremos con más detalle, pero conviene conocer la idea general desde el principio:

  • Dummy: objeto que se pasa como argumento, pero la prueba no lo usa realmente.
  • Stub: devuelve respuestas preparadas para controlar el escenario.
  • Fake: implementación funcional, pero simplificada, como un repositorio en memoria.
  • Spy: registra cómo fue usado para que la prueba pueda inspeccionarlo.
  • Mock: permite configurar respuestas y verificar interacciones esperadas.

En Python, muchas veces usamos la palabra "mock" de manera amplia, pero conceptualmente no todos los dobles cumplen el mismo rol.

1.11 Cuándo usar un doble de prueba

Usar un doble de prueba suele ser conveniente cuando la dependencia real tiene alguna de estas características:

  • Es lenta, como una llamada HTTP o una consulta pesada.
  • Es impredecible, como la hora actual o un generador aleatorio.
  • Es costosa, como un servicio de pago, correo o mensajería.
  • Es difícil de preparar, como una base de datos con muchos datos relacionados.
  • Tiene efectos secundarios, como escribir archivos o enviar notificaciones.

El doble permite reemplazar esa dependencia por una versión controlada, específica para la prueba.

1.12 Cuándo no usar un doble de prueba

No todo debe ser mockeado. Si una función es pura y no tiene dependencias externas, normalmente conviene probarla directamente. También hay casos donde una prueba de integración con componentes reales aporta más valor que reemplazar todo por dobles.

Un abuso común consiste en mockear tantos detalles internos que la prueba termina verificando la implementación exacta, no el comportamiento. Cuando eso ocurre, cualquier refactorización pequeña rompe pruebas que en realidad no deberían fallar.

Un buen doble simplifica la prueba. Si la vuelve más confusa que el código real, probablemente hay que revisar el diseño.

1.13 Ejercicio práctico

Analiza la siguiente función:

def obtener_saludo_usuario(usuario_id, repositorio_usuarios):
    usuario = repositorio_usuarios.buscar_por_id(usuario_id)

    if usuario is None:
        return "Usuario no encontrado"

    return f"Hola, {usuario['nombre']}"

La función depende de un repositorio. Para probarla no necesitamos una base de datos real. Podemos crear un stub que devuelva un usuario y otro que devuelva None.

1.14 Solución posible del ejercicio

Una solución simple puede escribirse así:

from usuarios import obtener_saludo_usuario


class RepositorioConUsuarioStub:
    def buscar_por_id(self, usuario_id):
        return {"id": usuario_id, "nombre": "Ana"}


class RepositorioSinUsuarioStub:
    def buscar_por_id(self, usuario_id):
        return None


def test_obtener_saludo_para_usuario_existente():
    repositorio = RepositorioConUsuarioStub()

    saludo = obtener_saludo_usuario(10, repositorio)

    assert saludo == "Hola, Ana"


def test_obtener_saludo_para_usuario_inexistente():
    repositorio = RepositorioSinUsuarioStub()

    saludo = obtener_saludo_usuario(99, repositorio)

    assert saludo == "Usuario no encontrado"

Estas pruebas no consultan una base de datos. Controlan la respuesta del repositorio y verifican el comportamiento de la función frente a los dos escenarios importantes.

1.15 Conclusión

Un doble de prueba es un reemplazo controlado de una dependencia real. Nos ayuda a escribir pruebas rápidas, claras y determinísticas cuando el código depende de servicios externos, datos difíciles de preparar o componentes con efectos secundarios.

En este tema usamos stubs manuales para controlar respuestas. En los próximos temas diferenciaremos con mayor precisión los tipos de dobles y luego usaremos herramientas propias de Python, como unittest.mock, patch, MagicMock y pytest.monkeypatch.