curso
Tutorial de pytest-mock: Guía de iniciación a la simulación en Python
A medida que tus proyectos Python crecen en complejidad y tamaño, también lo hace la importancia de unas pruebas sólidas. Las pruebas ayudan a garantizar que tu código funciona como se espera y mantiene la calidad en todas las partes de la aplicación.
Cuando tu aplicación interactúa con distintos tipos de bases de datos o API externas como OpenAI GPT o Anthropic Claude, probar estas dependencias puede convertirse en todo un reto. Aquí es donde la burla resulta útil.
La imitación te permite simular objetos, funciones y comportamientos, haciendo que tus pruebas sean más aisladas y predecibles. También te permite centrarte en la función específica que quieres probar, aislándola de dependencias que podrían introducir complejidad o un comportamiento inesperado.
En este blog, aprenderás a utilizar pytest-mock
, un potente plugin para pytest, para implementar mocks en tus pruebas de Python de forma eficiente. Alfinal de este tutorial, estarás preparado para añadir técnicas de simulación a tus casos de prueba, haciéndolos más potentes.
¿Qué es pytest-mock?
pytest-mock es un complemento para el popular marco de pruebas pytest de Python que proporciona un acceso sencillo a las funciones de imitación (mocking). Se basa en la herramienta integrada de Python unittest
, simplificando el proceso de simulación durante las pruebas.
logotipo del marco pytest
pytest-mock
mejora la legibilidad y facilita la implementación de mocks en las pruebas con un enfoque más nativo de la pirámide. Tanto si necesitas simular funciones individuales, métodos de clase u objetos enteros, pytest-mock
proporciona la flexibilidad necesaria para realizar pruebas eficaces sin la complejidad adicional.
Una razón clave de la legibilidad de pytest-mock
es su estilo de pruebas declarativo, que te permite especificar las llamadas y comportamientos esperados directamente en tus pruebas. Esto hace que el código sea más fácil de escribir, leer y mantener, al tiempo que sigue siendo resistente a los cambios de implementación que no alteran el comportamiento externo del código.
Conviértete en Desarrollador Python
Configuración de pytest-mock
Crear un entorno virtual para tu proyecto es una buena práctica para garantizar el aislamiento de las dependencias. Si aún no has configurado un nuevo entorno, puedes crear uno conda los siguientes comandos:
# create a conda environment
conda create --name yourenvname python=3.11
# activate conda environment
conda activate yourenvname
Instalación de pytest y pytest-mock
Instala pytest
y pytest-mock
directamente en tu entorno utilizando pip o conda:
pip install pytest
pip install pytest-mock
Confirmación de la instalación
Después de instalar pytest
y pytest-mock
, comprueba que la instalación se ha realizado correctamente ejecutando el siguiente comando:
pytest --version
Conceptos básicos de Mocking con pytest-mock
La simulación puede considerarse como la creación de una versión "ficticia" de un componente que imita el comportamiento real.
Es especialmente útil cuando necesitas probar cómo interactúan las funciones o clases con componentes externos, como bases de datos o API externas.
Introducción a la burla
Un objeto simulado simula el comportamiento de un objeto real. Suele utilizarse en las pruebas unitarias para aislar componentes y probar su comportamiento sin ejecutar código dependiente.
Por ejemplo, puedes utilizar un objeto simulado para simular una llamada a la base de datos sin conectarte realmente a ella.
La burla es especialmente útil cuando el objeto real:
- Es difícil de crear o configurar.
- Lleva mucho tiempo utilizarla (por ejemplo, acceder a una base de datos remota).
- Tiene efectos secundarios que deben evitarse durante las pruebas (por ejemplo, enviar correos electrónicos, incurrir en gastos).
Utilizar el accesorio simulador
pytest-mock proporciona un fixture mocker
, que facilita la creación y el control de objetos simulados. El fixture mocker
es powerful y se integra en tus pruebas pytest.
💡¿Qué son las fijaciones?En Python, los fixtures son componentes reutilizables que montan y desmontan recursos necesarios para las pruebas, como bases de datos o archivos. Garantizan la coherencia, reducen la duplicación y simplifican la configuración de las pruebas. |
Veamos un ejemplo:
def fetch_weather_data(api_client):
response = api_client.get("https://api.weather.com/data")
if response.status_code == 200:
return response.json()
else:
return None
La función fetch_weather_data
depende de una API meteorológica externa para recuperar los datos, pero llamar a la API durante las pruebas podría incurrir en costes innecesarios.
Para evitarlo, puedes utilizar mocking para simular el comportamiento de la API en tus pruebas, asegurándote de que la aplicación se prueba sin hacer llamadas externas. He aquí cómo puede ayudar la simulación:
def test_fetch_weather_data(mocker):
# Create a mock API client
mock_api_client = mocker.Mock()
# Mock the response of the API client
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": "20C", "condition": "Sunny"}
# Set the mock API client to return the mocked response
mock_api_client.get.return_value = mock_response
# Call the function with the mock API client
result = fetch_weather_data(mock_api_client)
# Assert that the correct data is returned
assert result == {"temperature": "20C", "condition": "Sunny"}
mock_api_client.get.assert_called_once_with("https://api.weather.com/data")
En test_fetch_weather_data()
, utilizamos el fixture mocker
para crear un cliente API simulado mock_api_client
y un mock_response
. La respuesta simulada está configurada para imitar una llamada real a la API devolviendo un código de estado 200 y datos JSON que contienen información meteorológica.
El método get del cliente API simulado está configurado para devolver esta respuesta simulada. Cuando se prueba fetch_weather_data()
, se interactúa con el cliente simulado de la API en lugar de realizar una llamada real a la API.
Este enfoque garantiza que la función se comporta como se espera, al tiempo que mantiene las pruebas rápidas, rentables e independientes de sistemas externos.
La sentencia assert del final verifica que los datos devueltos coinciden con la salida esperada.
La última línea, mock_api_client.get.assert_called_once_with("https://api.weather.com/data")
, comprueba específicamente que el método get del simulacro de cliente de la API se ha llamado exactamente una vez y con el argumento correcto. Esto ayuda a confirmar que la función está interactuando con la API de la forma esperada, añadiendo una capa extra de validación a la prueba.
Casos de uso común de pytest-mock
La simulación es útil en varios escenarios, incluidas las pruebas unitarias en las que necesitas aislar funciones o componentes específicos. Veamos algunos casos de uso habituales:
1. Burlarse de funciones o métodos de clase
Puede que necesites probar una función que depende de otras funciones o métodos, pero quieres controlar su comportamiento durante la prueba. La simulación te permite sustituir esas dependencias por un comportamiento predefinido:
# production code
def calculate_discount(price, discount_provider):
discount = discount_provider.get_discount()
return price - (price * discount / 100)
# test code
def test_calculate_discount(mocker):
# Mock the get_discount method
mock_discount_provider = mocker.Mock()
mock_discount_provider.get_discount.return_value = 10 # Mocked discount value
# Call the function with the mocked dependency
result = calculate_discount(100, mock_discount_provider)
# Assert the calculated discount is correct
assert result == 90
mock_discount_provider.get_discount.assert_called_once()
En esta prueba, se simula el método get_discount()
de discount_provider
para que devuelva un valor predefinido (10%). Esto aísla la función calculate_discount()
de la implementación real de discount_provider
.
2. Burlarse del código dependiente del tiempo
Al probar funciones que implican tiempo o retrasos, simular métodos relacionados con el tiempo ayuda a evitar la espera o la manipulación del tiempo real:
# production code
import time
def long_running_task():
time.sleep(5) # Simulate a long delay
return "Task Complete"
# test code
def test_long_running_task(mocker):
# Mock the sleep function in the time module
mocker.patch("time.sleep", return_value=None)
# Call the function
result = long_running_task()
# Assert the result is correct
assert result == "Task Complete"
Aquí, time.sleep
está parcheado para eludir el retraso real durante las pruebas. Esto garantiza que la prueba se ejecute rápidamente sin esperar 5 segundos.
3. Burlarse de los atributos de los objetos
A veces, puedes querer simular el comportamiento de los atributos o propiedades de un objeto para simular diferentes estados durante las pruebas:
# production code
class User:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def is_adult(self):
return self.age >= 18
# test code
def test_user_is_adult(mocker):
# Create a User object
user = User(name="Alice", age=17)
# Mock the is_adult property
mocker.patch.object(User, "is_adult", new_callable=mocker.PropertyMock, return_value=True)
# Assert the mocked property value
assert user.is_adult is True
En este ejemplo, se burla de la propiedad is_adult()
de la clase User
para que devuelva True, independientemente de la edad real. Esto es útil para probar escenarios que dependen de los estados de los objetos.
Técnicas avanzadas de simulación con pytest-mock
Una vez que te sientas cómodo con el mocking básico, pytest-mock
ofrece capacidades avanzadas para manejar escenarios más complejos.
1. Efectos secundarios simulados
Los simuladores pueden simular algo más que valores de retorno: también pueden reproducir comportamientos como lanzar excepciones o cambiar dinámicamente los valores de retorno. Esto se consigue añadiendo "efectos secundarios" a los simulacros:
import pytest
# production code
def process_payment(payment_gateway, amount):
response = payment_gateway.charge(amount)
if response == "Success":
return "Payment processed successfully"
else:
raise ValueError("Payment failed")
# test code
def test_process_payment_with_side_effects(mocker):
# Mock the charge method of the payment gateway
mock_payment_gateway = mocker.Mock()
# Add side effects: Success on first call, raise exception on second call
mock_payment_gateway.charge.side_effect = ["Success", ValueError("Insufficient funds")]
# Test successful payment
assert process_payment(mock_payment_gateway, 100) == "Payment processed successfully"
# Test payment failure
with pytest.raises(ValueError, match="Insufficient funds"):
process_payment(mock_payment_gateway, 200)
# Verify the mock's behavior
assert mock_payment_gateway.charge.call_count == 2
La propiedad side_effect
permite al simulacro devolver valores diferentes o lanzar excepciones en llamadas posteriores. En este ejemplo, la primera llamada a cargar devuelve "Éxito", mientras que la segunda llamada genera un ValueError
. Esto es útil para probar varios escenarios en una sola prueba.
2. Espiando las funciones
Espiando te permite rastrear cómo se ha llamado a una función real, incluyendo el número de llamadas y los argumentos pasados. Es especialmente útil cuando quieres asegurarte de que se llama a una función como se esperaba, sin dejar de ejecutar su lógica original:
# production code
def log_message(logger, message):
logger.info(message)
return f"Logged: {message}"
# test code
def test_log_message_with_spy(mocker):
# Spy on the info method of the logger
mock_logger = mocker.Mock()
spy_info = mocker.spy(mock_logger, "info")
# Call the function
result = log_message(mock_logger, "Test message")
# Assert the returned value
assert result == "Logged: Test message"
# Verify the spy behavior
spy_info.assert_called_once_with("Test message")
assert spy_info.call_count == 1
El método spy()
hace un seguimiento de cómo se llama a un método real sin alterar su comportamiento. Aquí, espiamos el método info del registrador para asegurarnos de que se llama con el mensaje correcto, al tiempo que dejamos que el método se ejecute normalmente.
Buenas prácticas para utilizar pytest-mock
La simulación es potente, pero utilizarla sin cuidado puede provocar problemas en tus pruebas. Aquí tienes algunas buenas prácticas que te recomiendo seguir:
1. Evita la sobreactuación
Utilizar demasiados mocks puede dar lugar a pruebas frágiles que se rompen cuando cambia el código interno, aunque no cambie el comportamiento externo. Esfuérzate por simular sólo lo necesario y confía en implementaciones reales siempre que sea posible.
La sobreactuación puede hacer que tus pruebas estén estrechamente vinculadas a los detalles de implementación de tu código, lo que significa que incluso una refactorización menor puede hacer que las pruebas fallen innecesariamente. En su lugar, céntrate en burlar sólo aquellas partes del sistema que sean externas o tengan efectos secundarios significativos.
2. Utiliza convenciones de nomenclatura claras
Cuando imites objetos, utiliza nombres descriptivos para tus imitaciones. Esto hace que tus pruebas sean más legibles y fáciles de mantener. Por ejemplo, en lugar de utilizar nombres genéricos como mock_function()
, utiliza algo más descriptivo, como mock_api_response()
.
Unas convenciones de nomenclatura claras ayudan a los demás a comprender la finalidad de cada simulacro, reduciendo la confusión y facilitando el mantenimiento del conjunto de pruebas a lo largo del tiempo.
3. Haz pruebas sencillas y centradas
Cada prueba debe centrarse en un único comportamiento o escenario. Esto simplifica la depuración y hace que tus pruebas sean fáciles de entender. Una prueba bien enfocada tiene más probabilidades de proporcionar información clara cuando algo va mal, lo que facilita la identificación y solución de los problemas.
Conclusión
En este blog, exploramos los fundamentos de la simulación con pytest-mock
y aprendimos a utilizarla para mejorar nuestras pruebas en Python. Lo cubrimos todo, desde la simulación básica de funciones y métodos hasta técnicas más avanzadas como añadir efectos secundarios y espiar funciones.
La simulación es una herramienta esencial para crear pruebas fiables y mantenibles, especialmente cuando se trabaja con sistemas complejos o dependencias externas. Si incorporas pytest-mock
a tus proyectos, podrás escribir pruebas más aisladas, predecibles y fáciles de mantener.
Para seguir ampliando tus conocimientos sobre pruebas, te recomiendo que consultesel curso gratuito Introducción a las Pruebas en Python en Datacamp.
Desarrolla habilidades de aprendizaje automático
Preguntas frecuentes
¿En qué se diferencia la simulación de probar componentes reales?
La simulación sustituye los componentes reales por otros simulados, lo que permite realizar pruebas aisladas y rentables sin depender de sistemas externos o datos reales.
¿Puedo utilizar pytest-mock con otros marcos de pruebas?
pytest-mock
está diseñado específicamente para pytest
y se integra perfectamente con él, pero se basa en unittest.mock
de Python, que puede utilizarse de forma independiente.
¿Son obligatorios los fixtures para mocking en pytest-mock?
Aunque los accesorios como mocker mejoran la eficacia de la simulación, puedes seguir utilizando unittest.mock
directamente en pytest
sin accesorios, aunque con menos integración.
¡Aprende más sobre Python con estos cursos!
curso
Introduction to Data Science in Python
curso
Introduction to Python for Developers
tutorial
Cómo utilizar Pytest para pruebas unitarias
tutorial
Configurar VSCode para Python: Guía completa
tutorial
Tutorial de FastAPI: Introducción al uso de FastAPI
tutorial
Tutorial de SQLAlchemy con ejemplos
tutorial
Guía de torchchat de PyTorch: Configuración local con Python
François Aubry
tutorial