3. Preparación de un proyecto Python para practicar mocking con pytest

3.1 Objetivo del tema

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.

Objetivo práctico: dejar listo un proyecto Python para ejecutar pruebas con pytest y escribir dobles de prueba en los próximos temas.

3.2 Requisitos previos

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.

3.3 Crear la carpeta del proyecto

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.

3.4 Crear un entorno virtual

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.

3.5 Instalar pytest

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.

3.6 Estructura de carpetas

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.

3.7 Crear las carpetas y archivos

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

3.8 Configurar 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.

3.9 Primer módulo de ejemplo

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.

3.10 Primera prueba con un stub manual

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.

3.11 Ejecutar las pruebas

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

3.12 Agregar el caso negativo

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.

3.13 Ejecutar una prueba específica

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.

3.14 Organización recomendada para los ejemplos

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.

3.15 Buenas prácticas desde el inicio

  • Usa nombres de prueba que describan el comportamiento esperado.
  • Mantén los stubs simples y cercanos al escenario de prueba.
  • No uses una dependencia real si el objetivo de la prueba no es verificar esa dependencia.
  • Evita preparar datos innecesarios para el comportamiento que estás probando.
  • Ejecuta las pruebas con frecuencia mientras escribes el código.

3.16 Ejercicio práctico

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.

3.17 Solución posible del ejercicio

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.

3.18 Conclusió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.