27. Uso de monkeypatch en pytest

27.1 Objetivo del tema

monkeypatch es una fixture de pytest que permite modificar temporalmente funciones, atributos, variables de entorno y el directorio de trabajo durante una prueba.

Al terminar la prueba, pytest revierte automáticamente esos cambios.

Idea clave: monkeypatch cambia algo solo durante una prueba y luego lo deja como estaba.

27.2 Crear una carpeta de práctica

Crea un proyecto nuevo:

mkdir pytest-monkeypatch-demo
cd pytest-monkeypatch-demo

Si pytest no está instalado en el entorno activo:

python -m pip install pytest

27.3 Crear un módulo de configuración

Crea un archivo llamado configuracion.py:

import os
from pathlib import Path


def obtener_modo():
    return os.getenv("APP_MODO", "desarrollo")


def obtener_api_key():
    api_key = os.getenv("API_KEY")
    if api_key is None:
        raise RuntimeError("Falta API_KEY")
    return api_key


def ruta_configuracion():
    return Path.cwd() / "config.ini"

Este módulo lee variables de entorno y usa el directorio actual.

27.4 Crear un módulo con funciones reemplazables

Crea un archivo llamado usuarios.py:

def obtener_usuario_desde_api(usuario_id):
    raise RuntimeError("No llamar a la API real durante una prueba")


def normalizar_nombre(nombre):
    return nombre.strip().title()


def construir_perfil(usuario_id):
    usuario = obtener_usuario_desde_api(usuario_id)
    return {
        "id": usuario["id"],
        "nombre": normalizar_nombre(usuario["nombre"]),
        "activo": usuario["activo"],
    }


class ClienteEmail:
    servidor = "smtp.real.example.com"

    def enviar(self, destinatario, asunto):
        raise RuntimeError("No enviar emails reales durante una prueba")


def notificar_bienvenida(cliente_email, email):
    cliente_email.enviar(email, "Bienvenido")
    return True

En las pruebas reemplazaremos la función que consulta la API y algunos atributos del cliente de email.

27.5 Cambiar variables de entorno con setenv

monkeypatch.setenv define una variable de entorno solo durante la prueba:

from configuracion import obtener_modo


def test_obtener_modo_desde_variable_de_entorno(monkeypatch):
    monkeypatch.setenv("APP_MODO", "produccion")

    assert obtener_modo() == "produccion"

Al terminar la prueba, APP_MODO vuelve a su estado anterior.

27.6 Eliminar variables de entorno con delenv

Para probar el valor por defecto, podemos asegurarnos de que la variable no exista:

def test_obtener_modo_por_defecto(monkeypatch):
    monkeypatch.delenv("APP_MODO", raising=False)

    assert obtener_modo() == "desarrollo"

raising=False evita error si la variable ya no estaba definida.

27.7 Probar error por variable faltante

También podemos probar una configuración obligatoria:

import pytest

from configuracion import obtener_api_key


def test_obtener_api_key_faltante_lanza_error(monkeypatch):
    monkeypatch.delenv("API_KEY", raising=False)

    with pytest.raises(RuntimeError):
        obtener_api_key()

27.8 Cambiar el directorio de trabajo con chdir

monkeypatch.chdir cambia temporalmente la carpeta actual:

from configuracion import ruta_configuracion


def test_ruta_configuracion_usa_directorio_actual(monkeypatch, tmp_path):
    monkeypatch.chdir(tmp_path)

    assert ruta_configuracion() == tmp_path / "config.ini"

Esto es útil para probar código que usa Path.cwd() o rutas relativas.

27.9 Reemplazar una función con setattr

Con monkeypatch.setattr podemos reemplazar una función por otra durante la prueba:

import usuarios


def test_construir_perfil_reemplaza_api(monkeypatch):
    def obtener_usuario_falso(usuario_id):
        return {
            "id": usuario_id,
            "nombre": "  ana  ",
            "activo": True,
        }

    monkeypatch.setattr(usuarios, "obtener_usuario_desde_api", obtener_usuario_falso)

    perfil = usuarios.construir_perfil(10)

    assert perfil == {
        "id": 10,
        "nombre": "Ana",
        "activo": True,
    }

La API real no se llama. La prueba controla la respuesta.

27.10 Reemplazar por una función lambda

Si la sustitución es pequeña, una lambda puede ser suficiente:

def test_construir_perfil_usuario_inactivo(monkeypatch):
    monkeypatch.setattr(
        usuarios,
        "obtener_usuario_desde_api",
        lambda usuario_id: {
            "id": usuario_id,
            "nombre": "Luis",
            "activo": False,
        },
    )

    perfil = usuarios.construir_perfil(20)

    assert perfil["activo"] is False

27.11 Reemplazar atributos de clase

monkeypatch.setattr también sirve para atributos:

from usuarios import ClienteEmail


def test_reemplazar_servidor_de_email(monkeypatch):
    monkeypatch.setattr(ClienteEmail, "servidor", "smtp.test.local")

    cliente = ClienteEmail()

    assert cliente.servidor == "smtp.test.local"

El cambio se revierte automáticamente al finalizar la prueba.

27.12 Reemplazar un método

Podemos reemplazar un método para evitar acciones reales:

from usuarios import notificar_bienvenida


def test_notificar_bienvenida(monkeypatch):
    enviados = []

    def enviar_falso(self, destinatario, asunto):
        enviados.append({
            "destinatario": destinatario,
            "asunto": asunto,
        })

    monkeypatch.setattr(ClienteEmail, "enviar", enviar_falso)

    cliente = ClienteEmail()
    resultado = notificar_bienvenida(cliente, "ana@example.com")

    assert resultado is True
    assert enviados == [
        {
            "destinatario": "ana@example.com",
            "asunto": "Bienvenido",
        }
    ]

El método falso recibe self igual que un método normal.

27.13 monkeypatch frente a patch

patch y monkeypatch resuelven problemas parecidos, pero tienen estilos distintos:

  • patch viene de unittest.mock y crea mocks con métodos de verificación.
  • monkeypatch es una fixture de pytest y reemplaza valores directamente.
  • monkeypatch es muy cómodo para variables de entorno, directorios y reemplazos simples.

27.14 Archivo completo de pruebas

El archivo test_monkeypatch.py puede quedar así:

import pytest

import usuarios
from configuracion import obtener_api_key, obtener_modo, ruta_configuracion
from usuarios import ClienteEmail, notificar_bienvenida


def test_obtener_modo_desde_variable_de_entorno(monkeypatch):
    monkeypatch.setenv("APP_MODO", "produccion")

    assert obtener_modo() == "produccion"


def test_obtener_modo_por_defecto(monkeypatch):
    monkeypatch.delenv("APP_MODO", raising=False)

    assert obtener_modo() == "desarrollo"


def test_obtener_api_key_faltante_lanza_error(monkeypatch):
    monkeypatch.delenv("API_KEY", raising=False)

    with pytest.raises(RuntimeError):
        obtener_api_key()


def test_ruta_configuracion_usa_directorio_actual(monkeypatch, tmp_path):
    monkeypatch.chdir(tmp_path)

    assert ruta_configuracion() == tmp_path / "config.ini"


def test_construir_perfil_reemplaza_api(monkeypatch):
    def obtener_usuario_falso(usuario_id):
        return {
            "id": usuario_id,
            "nombre": "  ana  ",
            "activo": True,
        }

    monkeypatch.setattr(usuarios, "obtener_usuario_desde_api", obtener_usuario_falso)

    perfil = usuarios.construir_perfil(10)

    assert perfil == {
        "id": 10,
        "nombre": "Ana",
        "activo": True,
    }


def test_construir_perfil_usuario_inactivo(monkeypatch):
    monkeypatch.setattr(
        usuarios,
        "obtener_usuario_desde_api",
        lambda usuario_id: {
            "id": usuario_id,
            "nombre": "Luis",
            "activo": False,
        },
    )

    perfil = usuarios.construir_perfil(20)

    assert perfil["activo"] is False


def test_reemplazar_servidor_de_email(monkeypatch):
    monkeypatch.setattr(ClienteEmail, "servidor", "smtp.test.local")

    cliente = ClienteEmail()

    assert cliente.servidor == "smtp.test.local"


def test_notificar_bienvenida(monkeypatch):
    enviados = []

    def enviar_falso(self, destinatario, asunto):
        enviados.append({
            "destinatario": destinatario,
            "asunto": asunto,
        })

    monkeypatch.setattr(ClienteEmail, "enviar", enviar_falso)

    cliente = ClienteEmail()
    resultado = notificar_bienvenida(cliente, "ana@example.com")

    assert resultado is True
    assert enviados == [
        {
            "destinatario": "ana@example.com",
            "asunto": "Bienvenido",
        }
    ]

27.15 Ejecutar las pruebas

Desde la raíz del proyecto, ejecuta:

python -m pytest

La salida esperada será similar a:

collected 8 items

test_monkeypatch.py ........                                    [100%]

8 passed in 0.05s

27.16 Métodos frecuentes de monkeypatch

  • monkeypatch.setattr(objeto, nombre, valor): reemplaza un atributo.
  • monkeypatch.delattr(objeto, nombre): elimina temporalmente un atributo.
  • monkeypatch.setenv(nombre, valor): define una variable de entorno.
  • monkeypatch.delenv(nombre, raising=False): elimina una variable de entorno.
  • monkeypatch.chdir(ruta): cambia el directorio de trabajo.

27.17 Errores frecuentes

  • Reemplazar el nombre equivocado: igual que con patch, debes cambiar lo que usa el módulo bajo prueba.
  • Olvidar raising=False: al eliminar una variable que puede no existir, evita errores innecesarios.
  • Reemplazar métodos sin self: un método falso de instancia debe aceptar self.
  • Usar monkeypatch para todo: si solo necesitas inyectar una dependencia por parámetro, puede ser más claro hacerlo directamente.
  • No probar el valor por defecto: las variables de entorno suelen necesitar pruebas con variable presente y ausente.

27.18 Comandos usados en este tema

mkdir pytest-monkeypatch-demo
cd pytest-monkeypatch-demo
python -m pip install pytest
python -m pytest
python -m pytest -v
python -m pytest test_monkeypatch.py::test_construir_perfil_reemplaza_api -v

27.19 Qué debes recordar de este tema

  • monkeypatch es una fixture de pytest.
  • Permite reemplazar atributos, funciones y métodos temporalmente.
  • También sirve para variables de entorno y directorio actual.
  • Los cambios se revierten automáticamente después de cada prueba.
  • Para métodos de instancia, la función falsa debe aceptar self.
  • Es una herramienta práctica para aislar código de dependencias externas.

27.20 Conclusión

En este tema usamos monkeypatch para controlar el entorno de ejecución de una prueba: variables de entorno, rutas, funciones, atributos y métodos.

En el próximo tema aplicaremos estas ideas a módulos que usan requests, tiempo o variables de entorno.