En el tema anterior construimos dobles de prueba manuales. Python incluye una herramienta para crear mocks sin escribir clases auxiliares: unittest.mock.
En este tema usaremos Mock para reemplazar dependencias, preparar valores de retorno, simular errores y verificar llamadas.
Mock sirve para controlar y observar una dependencia durante una prueba.
Crea un proyecto nuevo:
mkdir pytest-mock-basico-demo
cd pytest-mock-basico-demo
unittest.mock viene incluido con Python. Para ejecutar las pruebas instalaremos pytest si no está disponible:
python -m pip install pytest
Crea un archivo llamado reservas.py:
class ServicioReservas:
def __init__(self, repositorio, notificador):
self.repositorio = repositorio
self.notificador = notificador
def crear_reserva(self, cliente, habitacion, noches):
if noches <= 0:
raise ValueError("La cantidad de noches debe ser positiva")
if not self.repositorio.habitacion_disponible(habitacion):
raise ValueError("La habitación no está disponible")
reserva = {
"cliente": cliente,
"habitacion": habitacion,
"noches": noches,
"estado": "confirmada",
}
reserva_guardada = self.repositorio.guardar(reserva)
self.notificador.enviar(
cliente,
f"Reserva {reserva_guardada['id']} confirmada",
)
return reserva_guardada
def cancelar_reserva(self, reserva_id):
reserva = self.repositorio.obtener_por_id(reserva_id)
if reserva is None:
raise ValueError("Reserva inexistente")
reserva["estado"] = "cancelada"
self.repositorio.guardar(reserva)
self.notificador.enviar(
reserva["cliente"],
f"Reserva {reserva_id} cancelada",
)
return reserva
El servicio depende de un repositorio y de un notificador. En las pruebas los reemplazaremos por mocks.
Importamos Mock desde unittest.mock:
from unittest.mock import Mock
Un mock puede tener métodos creados dinámicamente. Por ejemplo:
repositorio = Mock()
repositorio.habitacion_disponible.return_value = True
Cuando el código llame a repositorio.habitacion_disponible(...), devolverá True.
Probemos una reserva exitosa:
from unittest.mock import Mock
from reservas import ServicioReservas
def test_crear_reserva_confirmada():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {
"id": 1,
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
resultado = servicio.crear_reserva("Ana", "101", 3)
assert resultado["id"] == 1
assert resultado["estado"] == "confirmada"
return_value define qué devuelve una llamada al mock.
Además del resultado, podemos verificar cómo se usaron las dependencias:
def test_crear_reserva_llama_a_dependencias():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {
"id": 1,
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
servicio.crear_reserva("Ana", "101", 3)
repositorio.habitacion_disponible.assert_called_once_with("101")
notificador.enviar.assert_called_once_with(
"Ana",
"Reserva 1 confirmada",
)
Esto confirma que el servicio consultó disponibilidad y envió la notificación esperada.
Podemos revisar el diccionario que se pasó a guardar:
def test_crear_reserva_guarda_datos_correctos():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {"id": 1}
servicio = ServicioReservas(repositorio, notificador)
servicio.crear_reserva("Ana", "101", 3)
repositorio.guardar.assert_called_once_with({
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
})
Esta prueba se concentra en el contrato entre el servicio y el repositorio.
Si la habitación no está disponible, no se debe guardar ni notificar:
import pytest
def test_crear_reserva_sin_disponibilidad_no_guarda_ni_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = False
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(ValueError):
servicio.crear_reserva("Ana", "101", 3)
repositorio.guardar.assert_not_called()
notificador.enviar.assert_not_called()
assert_not_called ayuda a verificar que una acción no ocurrió.
side_effect permite lanzar excepciones desde un mock:
def test_crear_reserva_si_guardar_falla_no_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.side_effect = RuntimeError("Error de base de datos")
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(RuntimeError):
servicio.crear_reserva("Ana", "101", 3)
notificador.enviar.assert_not_called()
Esta prueba confirma que no se envía una notificación si el guardado falla.
side_effect también puede recibir una lista de respuestas:
def test_mock_con_varias_respuestas():
generador = Mock()
generador.codigo.side_effect = ["A1", "B2", "C3"]
assert generador.codigo() == "A1"
assert generador.codigo() == "B2"
assert generador.codigo() == "C3"
Cada llamada consume el siguiente valor de la lista.
Para cancelar una reserva, preparamos lo que devuelve obtener_por_id:
def test_cancelar_reserva_actualiza_y_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.obtener_por_id.return_value = {
"id": 5,
"cliente": "Luis",
"habitacion": "202",
"noches": 2,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
resultado = servicio.cancelar_reserva(5)
assert resultado["estado"] == "cancelada"
repositorio.guardar.assert_called_once_with(resultado)
notificador.enviar.assert_called_once_with(
"Luis",
"Reserva 5 cancelada",
)
Si el repositorio no encuentra la reserva, no debe guardar ni notificar:
def test_cancelar_reserva_inexistente_lanza_error():
repositorio = Mock()
notificador = Mock()
repositorio.obtener_por_id.return_value = None
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(ValueError):
servicio.cancelar_reserva(99)
repositorio.guardar.assert_not_called()
notificador.enviar.assert_not_called()
El archivo test_reservas.py puede quedar así:
from unittest.mock import Mock
import pytest
from reservas import ServicioReservas
def test_crear_reserva_confirmada():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {
"id": 1,
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
resultado = servicio.crear_reserva("Ana", "101", 3)
assert resultado["id"] == 1
assert resultado["estado"] == "confirmada"
def test_crear_reserva_llama_a_dependencias():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {
"id": 1,
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
servicio.crear_reserva("Ana", "101", 3)
repositorio.habitacion_disponible.assert_called_once_with("101")
notificador.enviar.assert_called_once_with(
"Ana",
"Reserva 1 confirmada",
)
def test_crear_reserva_guarda_datos_correctos():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.return_value = {"id": 1}
servicio = ServicioReservas(repositorio, notificador)
servicio.crear_reserva("Ana", "101", 3)
repositorio.guardar.assert_called_once_with({
"cliente": "Ana",
"habitacion": "101",
"noches": 3,
"estado": "confirmada",
})
def test_crear_reserva_sin_disponibilidad_no_guarda_ni_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = False
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(ValueError):
servicio.crear_reserva("Ana", "101", 3)
repositorio.guardar.assert_not_called()
notificador.enviar.assert_not_called()
def test_crear_reserva_si_guardar_falla_no_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.habitacion_disponible.return_value = True
repositorio.guardar.side_effect = RuntimeError("Error de base de datos")
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(RuntimeError):
servicio.crear_reserva("Ana", "101", 3)
notificador.enviar.assert_not_called()
def test_mock_con_varias_respuestas():
generador = Mock()
generador.codigo.side_effect = ["A1", "B2", "C3"]
assert generador.codigo() == "A1"
assert generador.codigo() == "B2"
assert generador.codigo() == "C3"
def test_cancelar_reserva_actualiza_y_notifica():
repositorio = Mock()
notificador = Mock()
repositorio.obtener_por_id.return_value = {
"id": 5,
"cliente": "Luis",
"habitacion": "202",
"noches": 2,
"estado": "confirmada",
}
servicio = ServicioReservas(repositorio, notificador)
resultado = servicio.cancelar_reserva(5)
assert resultado["estado"] == "cancelada"
repositorio.guardar.assert_called_once_with(resultado)
notificador.enviar.assert_called_once_with(
"Luis",
"Reserva 5 cancelada",
)
def test_cancelar_reserva_inexistente_lanza_error():
repositorio = Mock()
notificador = Mock()
repositorio.obtener_por_id.return_value = None
servicio = ServicioReservas(repositorio, notificador)
with pytest.raises(ValueError):
servicio.cancelar_reserva(99)
repositorio.guardar.assert_not_called()
notificador.enviar.assert_not_called()
Desde la raíz del proyecto, ejecuta:
python -m pytest
La salida esperada será similar a:
collected 8 items
test_reservas.py ........ [100%]
8 passed in 0.05s
return_value: define el valor devuelto por una llamada.side_effect: define una excepción, una función o una secuencia de respuestas.assert_called_once_with: verifica una única llamada con argumentos concretos.assert_not_called: verifica que el mock no haya sido llamado.call_count: indica cuántas veces fue llamado.call_args: permite inspeccionar los argumentos de la llamada más reciente.No todo necesita un mock. Si puedes probar una función pura directamente, hazlo. Si un fake simple en memoria expresa mejor el comportamiento, puede ser más claro que configurar muchos métodos en un mock.
Mock devuelve otro Mock por defecto.side_effect ayuda a probar fallos de dependencias.mkdir pytest-mock-basico-demo
cd pytest-mock-basico-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_reservas.py::test_crear_reserva_llama_a_dependencias -v
unittest.mock viene incluido con Python.Mock permite crear dobles de prueba rápidamente.return_value configura respuestas exitosas.side_effect permite simular errores o varias respuestas.assert_called... verifican interacciones.En este tema usamos unittest.mock.Mock para reemplazar dependencias, preparar respuestas, simular errores y verificar llamadas.
En el próximo tema profundizaremos en cómo mockear funciones, métodos y dependencias externas, prestando atención a dónde aplicar el reemplazo.