1 usuario conectado

Pruebas en Python.Aislamiento y control con Mocking

Comparte esto

En el ámbito de las pruebas unitarias, un desafío común surge cuando el código que queremos probar depende de otros componentes o servicios que son difíciles de controlar, lentos, no deterministas o aún no están implementados. Aquí es donde entra en juego la técnica de mocking.

Mocking consiste en reemplazar las dependencias reales de nuestro código bajo prueba con objetos simulados (los "mocks"). Estos mocks tienen un comportamiento predefinido que podemos controlar y verificar durante la ejecución de la prueba. Al aislar la unidad de código que estamos probando de sus dependencias, podemos centrarnos en verificar su lógica interna sin vernos afectados por el comportamiento o el estado de los componentes externos.

Python proporciona la librería unittest.mock (a partir de Python 3.3, anteriormente disponible como un módulo de terceros llamado mock) para facilitar la creación y el uso de mocks en nuestras pruebas.

¿Por qué usar Mocking?

Conceptos Clave de unittest.mock:

  1. Mock: La clase principal para crear objetos simulados. Un Mock es un objeto dinámico que puede tener atributos y métodos definidos sobre la marcha.

  2. MagicMock: Una subclase de Mock que proporciona implementaciones por defecto para la mayoría de los métodos mágicos de Python (como __str__, __len__, __iter__, etc.), lo que la hace más útil en muchos escenarios.

  3. patch: Un decorador o context manager que permite reemplazar temporalmente objetos en un módulo o clase con mocks durante la ejecución de una prueba. Es una forma conveniente de inyectar mocks en el código bajo prueba.

  4. Configuración de Comportamiento: Podemos configurar el comportamiento de nuestros mocks definiendo sus valores de retorno (return_value), lanzando excepciones (side_effect), o definiendo qué se devuelve cuando se acceden a sus atributos o se llaman a sus métodos.

  5. Verificación de Interacciones: Una de las funcionalidades clave del mocking es la capacidad de verificar cómo interactuó el código bajo prueba con sus dependencias mockeadas. Podemos verificar si se llamaron ciertos métodos, cuántas veces se llamaron y con qué argumentos.

Ejemplo Básico:

Supongamos que tenemos una función obtener_datos_remotos(url) que hace una llamada a una API externa y devuelve los datos:

Python
import requests

def obtener_datos_remotos(url):
    respuesta = requests.get(url)
    respuesta.raise_for_status()  # Lanza una excepción para códigos de error HTTP
    return respuesta.json()

Para probar una función que utiliza obtener_datos_remotos, sin hacer una llamada real a la API, podemos mockear la función requests.get:

Python
import unittest
from unittest.mock import patch
import mi_modulo  # Supongamos que obtener_datos_remotos está en mi_modulo.py

class TestObtenerDatosRemotos(unittest.TestCase):
    @patch('mi_modulo.requests.get')
    def test_obtener_datos_exito(self, mock_get):
        # Configurar el comportamiento del mock
        mock_respuesta = mock_get.return_value
        mock_respuesta.json.return_value = {'clave': 'valor'}
        mock_respuesta.raise_for_status.return_value = None

        # Llamar a la función bajo prueba
        datos = mi_modulo.obtener_datos_remotos('http://ejemplo.com/api')

        # Verificar el resultado
        self.assertEqual(datos, {'clave': 'valor'})

        # Verificar que se llamó a requests.get con la URL correcta
        mock_get.assert_called_once_with('http://ejemplo.com/api')

    @patch('mi_modulo.requests.get')
    def test_obtener_datos_error(self, mock_get):
        # Configurar el comportamiento del mock para simular un error
        mock_respuesta = mock_get.return_value
        mock_respuesta.raise_for_status.side_effect = requests.exceptions.HTTPError('Error 500')

        # Llamar a la función bajo prueba y verificar que lanza la excepción
        with self.assertRaises(requests.exceptions.HTTPError):
            mi_modulo.obtener_datos_remotos('http://ejemplo.com/api')

        # Verificar que se llamó a requests.get con la URL correcta
        mock_get.assert_called_once_with('http://ejemplo.com/api')

if __name__ == '__main__':
    unittest.main()

En este ejemplo:

Conclusión:

La librería unittest.mock es una herramienta esencial para escribir pruebas unitarias efectivas en Python, especialmente cuando el código bajo prueba interactúa con dependencias externas. Al utilizar mocks, podemos aislar nuestro código, acelerar las pruebas, simular escenarios complejos y verificar las interacciones con las dependencias, lo que conduce a pruebas más robustas y un software de mayor calidad. Dominar el arte del mocking es una habilidad valiosa para cualquier desarrollador de Python que se tome en serio la calidad de su código.