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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
En Python, muchas veces usamos la palabra "mock" de manera amplia, pero conceptualmente no todos los dobles cumplen el mismo rol.
Usar un doble de prueba suele ser conveniente cuando la dependencia real tiene alguna de estas características:
El doble permite reemplazar esa dependencia por una versión controlada, específica para la 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.
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.
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.
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.