Saltar al contenido principal

Tutorial de pytest-mock: Guía de iniciación a la simulación en Python

Aprende a utilizar pytest-mock, desde la configuración y los fundamentos hasta las técnicas avanzadas para realizar pruebas prácticas.
Actualizado 19 dic 2024  · 15 min de lectura

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

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

Adquiere los conocimientos de programación que necesitan todos los desarrolladores de Python.
Empieza a aprender gratis

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

Eleva tus habilidades de aprendizaje automático al nivel de producción.

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.


Moez Ali's photo
Author
Moez Ali
LinkedIn
Twitter

Científico de Datos, Fundador y Creador de PyCaret

Temas

¡Aprende más sobre Python con estos cursos!

curso

Introduction to Functions in Python

3 hr
433K
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.
Ver detallesRight Arrow
Comienza el curso
Ver másRight Arrow
Relacionado

tutorial

Cómo utilizar Pytest para pruebas unitarias

Explore qué es Pytest y para qué se utiliza mientras lo compara con otros métodos de prueba de software.
Kurtis Pykes 's photo

Kurtis Pykes

17 min

tutorial

Configurar VSCode para Python: Guía completa

Experimenta una forma sencilla, divertida y productiva de desarrollar en Python conociendo VSCode y sus extensiones y funciones.
Abid Ali Awan's photo

Abid Ali Awan

16 min

tutorial

Tutorial de FastAPI: Introducción al uso de FastAPI

Explore el marco FastAPI y descubra cómo puede utilizarlo para crear API en Python.
Moez Ali's photo

Moez Ali

13 min

tutorial

Tutorial de SQLAlchemy con ejemplos

Aprende a acceder y ejecutar consultas SQL en todo tipo de bases de datos relacionales utilizando objetos Python.
Abid Ali Awan's photo

Abid Ali Awan

13 min

tutorial

Guía de torchchat de PyTorch: Configuración local con Python

Aprende a configurar el torchat de PyTorch localmente con Python en este tutorial práctico, que proporciona orientación paso a paso y ejemplos.
François Aubry's photo

François Aubry

tutorial

Tutorial de pandas en Python: la guía definitiva para principiantes

¿Todo preparado para comenzar tu viaje de pandas? Aquí tienes una guía paso a paso sobre cómo empezar.
Vidhi Chugh's photo

Vidhi Chugh

15 min

See MoreSee More