Antes de profundizar en unittest.mock, patch y monkeypatch, conviene preparar un proyecto pequeño y ordenado. Así podremos practicar con archivos reales, pruebas ejecutables y una estructura parecida a la que usaríamos en un proyecto profesional.
En este tema crearemos la carpeta del proyecto, un entorno virtual, la configuración mínima de pytest, un módulo de ejemplo y una primera prueba.
Para seguir los ejemplos se necesita tener instalado Python y poder ejecutar comandos desde la terminal. Puedes verificar la versión con:
python --version
En algunos sistemas el comando puede ser python3:
python3 --version
Los ejemplos del curso usan pytest y módulos de la biblioteca estándar de Python. Para mocking usaremos principalmente unittest.mock, que ya viene incluido con Python.
Crearemos un proyecto llamado tienda_mocking. Desde la terminal ejecuta:
mkdir tienda_mocking
cd tienda_mocking
Dentro de esta carpeta pondremos el código de aplicación y las pruebas.
Un entorno virtual permite instalar herramientas para este proyecto sin afectar otros proyectos de Python.
En Windows:
python -m venv .venv
.venv\Scripts\activate
En Linux o macOS:
python3 -m venv .venv
source .venv/bin/activate
Cuando el entorno está activado, la terminal suele mostrar (.venv) al comienzo de la línea.
Instalamos pytest dentro del entorno virtual:
pip install pytest
Luego verificamos que esté disponible:
pytest --version
No necesitamos instalar una biblioteca externa para Mock, MagicMock o patch, porque forman parte de unittest.mock.
Usaremos una estructura simple:
tienda_mocking/
├── pyproject.toml
├── src/
│ └── tienda/
│ ├── __init__.py
│ └── promociones.py
└── tests/
└── test_promociones.py
La carpeta src/tienda contendrá el código de la aplicación. La carpeta tests contendrá las pruebas.
Podemos crear la estructura con estos comandos:
mkdir src
mkdir src\tienda
mkdir tests
type nul > src\tienda\__init__.py
type nul > src\tienda\promociones.py
type nul > tests\test_promociones.py
type nul > pyproject.toml
En Linux o macOS, los comandos equivalentes son:
mkdir -p src/tienda tests
touch src/tienda/__init__.py
touch src/tienda/promociones.py
touch tests/test_promociones.py
touch pyproject.toml
Agregamos una configuración mínima para que pytest encuentre el código dentro de src:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-q"
Con esta configuración, las pruebas podrán importar módulos como tienda.promociones sin instalar todavía el paquete.
En el archivo src/tienda/promociones.py escribimos una función simple que luego nos servirá para practicar stubs:
def tiene_envio_gratis(cliente_id, servicio_compras):
total = servicio_compras.obtener_total_comprado(cliente_id)
return total >= 50000
La función no sabe cómo se obtiene el total comprado. Solo recibe una dependencia llamada servicio_compras y le pide el dato. Esa separación facilitará las pruebas.
En tests/test_promociones.py escribimos una prueba usando un stub:
from tienda.promociones import tiene_envio_gratis
class ServicioComprasStub:
def __init__(self, total):
self.total = total
def obtener_total_comprado(self, cliente_id):
return self.total
def test_cliente_tiene_envio_gratis_si_supera_el_minimo():
servicio = ServicioComprasStub(total=65000)
resultado = tiene_envio_gratis("CLI-100", servicio)
assert resultado is True
Esta prueba ya aplica la idea central del curso: reemplazar una dependencia real por una versión controlada para probar una unidad de código.
Desde la raíz del proyecto ejecutamos:
pytest
Si todo está bien, deberíamos ver una salida indicando que la prueba pasó. Como configuramos addopts = "-q", la salida será breve.
1 passed
Agregamos otra prueba para el caso en que el cliente no alcanza el mínimo:
def test_cliente_no_tiene_envio_gratis_si_no_supera_el_minimo():
servicio = ServicioComprasStub(total=12000)
resultado = tiene_envio_gratis("CLI-200", servicio)
assert resultado is False
Ahora el mismo stub configurable permite probar dos escenarios distintos sin depender de datos reales.
Durante el desarrollo es común ejecutar una sola prueba. Podemos hacerlo indicando el archivo y el nombre de la función:
pytest tests/test_promociones.py::test_cliente_tiene_envio_gratis_si_supera_el_minimo
Esto será útil cuando estemos ajustando un ejemplo concreto de mocking.
A medida que avance el curso, conviene mantener ejemplos pequeños y nombres claros. Una organización posible es crear un módulo por tema o por caso práctico:
src/tienda/
├── promociones.py
├── pedidos.py
├── usuarios.py
└── notificaciones.py
tests/
├── test_promociones.py
├── test_pedidos.py
├── test_usuarios.py
└── test_notificaciones.py
Esta separación evita que los ejemplos se mezclen y facilita ejecutar pruebas puntuales.
Crea un archivo src/tienda/clientes.py con esta función:
def es_cliente_vip(cliente_id, repositorio_clientes):
cliente = repositorio_clientes.buscar_por_id(cliente_id)
return cliente is not None and cliente["categoria"] == "vip"
Luego crea tests/test_clientes.py y escribe dos pruebas: una para un cliente VIP y otra para un cliente común o inexistente.
Una solución usando un stub configurable podría ser:
from tienda.clientes import es_cliente_vip
class RepositorioClientesStub:
def __init__(self, cliente):
self.cliente = cliente
def buscar_por_id(self, cliente_id):
return self.cliente
def test_cliente_vip_devuelve_true():
repositorio = RepositorioClientesStub(
{"id": 1, "nombre": "Ana", "categoria": "vip"}
)
assert es_cliente_vip(1, repositorio) is True
def test_cliente_comun_devuelve_false():
repositorio = RepositorioClientesStub(
{"id": 2, "nombre": "Luis", "categoria": "comun"}
)
assert es_cliente_vip(2, repositorio) is False
def test_cliente_inexistente_devuelve_false():
repositorio = RepositorioClientesStub(None)
assert es_cliente_vip(99, repositorio) is False
El stub controla qué devuelve el repositorio, y la prueba se concentra en la decisión que toma la función.
Ya tenemos una estructura mínima para practicar mocking en Python: código dentro de src, pruebas dentro de tests, configuración en pyproject.toml y ejecución con pytest.
En el próximo tema crearemos el primer stub manual para controlar una dependencia externa con más detalle, revisando cómo elegir los datos de prueba y cómo mantener el doble simple.