9. Configurar valores de retorno con return_value

9.1 Objetivo del tema

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.

Objetivo práctico: controlar respuestas de funciones y métodos simulados usando return_value de forma clara y segura.

9.2 return_value en una función mockeada

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"

9.3 Ejemplo con función inyectada

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.

9.4 return_value en un método

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")

9.5 Mock como stub

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.

return_value es ideal para respuestas simples. Si el comportamiento crece, un stub manual o un fake pueden ser más legibles.

9.6 Devolver diccionarios

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"

9.7 Devolver None

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.

9.8 Devolver listas

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.

9.9 Devolver objetos simples

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.

9.10 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"

9.11 Cuidado con cadenas de mocks

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.

Si configurar el mock requiere entender demasiados detalles internos, la prueba probablemente está demasiado acoplada.

9.12 Un objeto respuesta más claro

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.

9.13 return_value por defecto

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.

9.14 Error frecuente: configurar el mock equivocado

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

9.15 Verificar llamadas junto con return_value

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.

9.16 Ejercicio práctico

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.

9.17 Solución posible del ejercicio

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.

9.18 Conclusión

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.