return_value es una de las herramientas más usadas de unittest.mock. Permite indicar qué debe devolver un mock cuando se lo llama como función o cuando se invoca uno de sus métodos.
En este tema veremos cómo usarlo en casos simples y también en situaciones donde el mock devuelve objetos que luego serán usados por el código probado.
Un Mock puede representar una función. Al configurar return_value, indicamos qué devuelve al llamarlo:
from unittest.mock import Mock
generar_codigo = Mock(return_value="ABC123")
resultado = generar_codigo()
assert resultado == "ABC123"
También podemos asignarlo después de crear el mock:
generar_codigo = Mock()
generar_codigo.return_value = "ABC123"
Supongamos esta función:
def crear_codigo_promocional(cliente_id, generar_token):
token = generar_token()
return f"PROMO-{cliente_id}-{token}"
La prueba controla el token generado:
from unittest.mock import Mock
from tienda.promociones import crear_codigo_promocional
def test_crear_codigo_promocional():
generar_token = Mock(return_value="XYZ")
codigo = crear_codigo_promocional("CLI-10", generar_token)
assert codigo == "PROMO-CLI-10-XYZ"
generar_token.assert_called_once_with()
El valor aleatorio queda reemplazado por un valor fijo, fácil de verificar.
Cuando el mock representa un objeto, configuramos el return_value del método que será llamado:
servicio = Mock()
servicio.obtener_total_comprado.return_value = 75000
Ejemplo completo:
def tiene_envio_gratis(cliente_id, servicio_compras):
total = servicio_compras.obtener_total_comprado(cliente_id)
return total >= 50000
def test_tiene_envio_gratis():
servicio = Mock()
servicio.obtener_total_comprado.return_value = 75000
assert tiene_envio_gratis("CLI-1", servicio) is True
servicio.obtener_total_comprado.assert_called_once_with("CLI-1")
Cuando usamos return_value para devolver datos preparados, el mock está cumpliendo el rol de stub.
La ventaja es que no necesitamos escribir una clase manual. La desventaja es que la prueba puede volverse menos explícita si configuramos demasiadas cosas dentro del mock.
Muchas dependencias devuelven diccionarios. Podemos configurarlo directamente:
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"nombre": "Ana",
"categoria": "vip",
}
Ejemplo de prueba:
def obtener_categoria_cliente(cliente_id, repositorio):
cliente = repositorio.buscar_por_id(cliente_id)
if cliente is None:
return "desconocida"
return cliente["categoria"]
def test_obtener_categoria_cliente():
repositorio = Mock()
repositorio.buscar_por_id.return_value = {
"id": 1,
"nombre": "Ana",
"categoria": "vip",
}
categoria = obtener_categoria_cliente(1, repositorio)
assert categoria == "vip"
return_value también puede ser None, útil para simular que no se encontró un dato:
def test_obtener_categoria_cliente_inexistente():
repositorio = Mock()
repositorio.buscar_por_id.return_value = None
categoria = obtener_categoria_cliente(99, repositorio)
assert categoria == "desconocida"
Este caso aparece mucho al reemplazar repositorios o consultas a servicios externos.
También podemos devolver una lista preparada:
def contar_pedidos_pendientes(usuario_id, repositorio_pedidos):
pedidos = repositorio_pedidos.buscar_pendientes(usuario_id)
return len(pedidos)
def test_contar_pedidos_pendientes():
repositorio = Mock()
repositorio.buscar_pendientes.return_value = [
{"id": 1},
{"id": 2},
{"id": 3},
]
cantidad = contar_pedidos_pendientes("USR-1", repositorio)
assert cantidad == 3
La prueba no necesita preparar pedidos reales en una base de datos.
Si el código espera un objeto con atributos, podemos devolver una dataclass:
from dataclasses import dataclass
@dataclass
class Usuario:
email: str
activo: bool
Y usarla como return_value:
def puede_recibir_notificaciones(usuario_id, repositorio):
usuario = repositorio.buscar_por_id(usuario_id)
return usuario is not None and usuario.activo
def test_usuario_activo_puede_recibir_notificaciones():
repositorio = Mock()
repositorio.buscar_por_id.return_value = Usuario(
email="ana@example.com",
activo=True,
)
assert puede_recibir_notificaciones(1, repositorio) is True
Usar objetos reales simples puede ser más claro que devolver otro mock.
A veces el código obtiene un objeto y luego llama métodos sobre ese objeto. Podemos configurar un mock como valor de retorno:
cliente_api = Mock()
respuesta = Mock()
respuesta.json.return_value = {"estado": "ok"}
cliente_api.get.return_value = respuesta
Ejemplo:
def consultar_estado(cliente_api, url):
respuesta = cliente_api.get(url)
datos = respuesta.json()
return datos["estado"]
def test_consultar_estado():
cliente_api = Mock()
respuesta = Mock()
respuesta.json.return_value = {"estado": "ok"}
cliente_api.get.return_value = respuesta
estado = consultar_estado(cliente_api, "https://api.example.com/estado")
assert estado == "ok"
El ejemplo anterior es válido, pero si una prueba tiene muchas llamadas encadenadas puede volverse difícil de leer:
cliente.get.return_value.json.return_value.get.return_value = "ok"
Cuando aparece una cadena demasiado larga, puede ser señal de que conviene crear un objeto respuesta simple o un stub manual.
Podemos reemplazar una cadena de mocks por una clase pequeña:
class RespuestaApiStub:
def __init__(self, datos):
self.datos = datos
def json(self):
return self.datos
Y usarla como retorno:
def test_consultar_estado_con_respuesta_stub():
cliente_api = Mock()
cliente_api.get.return_value = RespuestaApiStub({"estado": "ok"})
estado = consultar_estado(cliente_api, "https://api.example.com/estado")
assert estado == "ok"
El mock sigue siendo útil para simular get, pero la respuesta queda más expresiva.
Si llamamos un Mock sin configurar return_value, devuelve otro Mock:
funcion = Mock()
resultado = funcion()
assert isinstance(resultado, Mock)
Esto puede ocultar errores. Una prueba podría seguir avanzando con mocks generados automáticamente en lugar de fallar donde esperábamos.
Un error común es configurar el return_value del objeto equivocado.
servicio = Mock(return_value=65000)
Esto configura qué devuelve servicio(), pero no qué devuelve servicio.obtener_total_comprado(). Si el código llama a un método, debemos configurar el método:
servicio = Mock()
servicio.obtener_total_comprado.return_value = 65000
Además de controlar la respuesta, podemos verificar que se llamó con los argumentos esperados:
def test_tiene_envio_gratis_verifica_cliente_consultado():
servicio = Mock()
servicio.obtener_total_comprado.return_value = 65000
tiene_envio_gratis("CLI-100", servicio)
servicio.obtener_total_comprado.assert_called_once_with("CLI-100")
Conviene hacer esta verificación cuando la interacción sea parte importante del comportamiento esperado.
Prueba esta función usando return_value:
def obtener_total_carrito(usuario_id, repositorio_carritos):
carrito = repositorio_carritos.buscar_por_usuario(usuario_id)
if carrito is None:
return 0
return sum(item["precio"] * item["cantidad"] for item in carrito["items"])
Escribe una prueba para un carrito con dos productos y otra para un usuario sin carrito.
Una solución con Mock:
from unittest.mock import Mock
from tienda.carritos import obtener_total_carrito
def test_obtener_total_carrito_con_productos():
repositorio = Mock()
repositorio.buscar_por_usuario.return_value = {
"usuario_id": "USR-1",
"items": [
{"producto": "Teclado", "precio": 1000, "cantidad": 2},
{"producto": "Mouse", "precio": 500, "cantidad": 1},
],
}
total = obtener_total_carrito("USR-1", repositorio)
assert total == 2500
repositorio.buscar_por_usuario.assert_called_once_with("USR-1")
def test_obtener_total_carrito_sin_carrito():
repositorio = Mock()
repositorio.buscar_por_usuario.return_value = None
total = obtener_total_carrito("USR-2", repositorio)
assert total == 0
El mock controla qué devuelve el repositorio y permite verificar la consulta realizada.
return_value permite controlar de manera directa qué devuelve un mock o uno de sus métodos. Es especialmente útil para simular funciones, repositorios, clientes HTTP y servicios externos.
En el próximo tema veremos side_effect, que permite simular errores, respuestas secuenciales y comportamientos más dinámicos.