Introducción a los mocks en Python: simula antes de crear
10 de febrero de 2018
0 minutos de lecturaNota del editor
Este blog se publicó originalmente en fugue.co. Fugue se unió a Snyk en 2022 y es un componente clave de Snyk IaC.
FugueTe damos la bienvenida a esta guía sobre los conceptos básicos de los mocks en Python. Surgió de mi necesidad de probar código que usaba muchos servicios de red y de mi experiencia con GoMock, que me mostró lo poderosos que pueden ser los mocks cuando se usan correctamente (gracias, Tyler). Empezaré con una reflexión filosófica sobre los mocks, porque hacer buenos mocks requiere una mentalidad distinta a la de desarrollar bien. Desarrollar consiste en crear cosas; hacer mocks, en fingirlas. Puede parecer obvio, pero el aspecto de «fingir» de las pruebas con mocks es fundamental, y comprenderlo por completo cambia la forma de ver las pruebas. Después veremos las herramientas de mocking que ofrece Python y terminaremos con un ejemplo completo. Conoce más sobre cómo probar código para la seguridad en Python con nuestra guía rápida.
Los mocks pueden ser difíciles de entender. Cuando pruebo el código que escribí, quiero comprobar si hace lo que se supone que debe hacer de principio a fin. Por lo general, empiezo pensando en una prueba funcional e integrada, en la que ingreso datos realistas y obtengo resultados realistas. Accedo a todos los sistemas reales que usa mi código para asegurarme de que las interacciones entre ellos funcionen correctamente, con objetos reales y llamadas a API reales. Aunque este tipo de pruebas es esencial para verificar que los sistemas complejos funcionen bien en conjunto, no es lo que buscamos en las pruebas unitarias.
Las pruebas unitarias se centran en la capa más externa del código. Las pruebas de integración son necesarias, pero las pruebas unitarias automatizadas que ejecutamos no deberían llegar a ese nivel de interacción entre sistemas. Esto significa que podemos y debemos reemplazar con mocks las llamadas a API que haya en la función que estamos probando. Debemos sustituir cualquier llamada a una API no trivial o creación de objetos por una llamada o un objeto mock. Así evitamos usar recursos innecesariamente, simplificamos la configuración de las pruebas y reducimos su tiempo de ejecución. Imagina que pruebas una función que accede a una API HTTP externa. En lugar de asegurarte de que haya un servidor de prueba disponible para enviar las respuestas correctas, puedes crear un mock de la biblioteca HTTP y reemplazar todas las llamadas HTTP por llamadas mock. Esto reduce la complejidad y las dependencias de las pruebas, y te permite controlar con precisión lo que devuelve la biblioteca HTTP, algo que de otro modo podría ser difícil de lograr.
¿Qué queremos decir con mocking?
El término mocking se usa con mucha frecuencia, pero en este documento usamos la siguiente definición:
«Reemplazar una o varias llamadas a funciones u objetos por llamadas u objetos mock»
Una llamada a una función mock devuelve de inmediato un valor predefinido, sin realizar ninguna operación. Los atributos y métodos de un objeto mock también se definen por completo en la prueba, sin crear el objeto real ni realizar ninguna operación. El hecho de que quien escribe la prueba pueda definir los valores que devuelve cada llamada a una función le da muchísimo control al probar, pero también significa que debe hacer un trabajo de base para configurar todo correctamente.
En Python, el mocking se realiza mediante el módulo unittest.mock. El módulo contiene varias clases y funciones útiles; las más importantes son la función patch (como decorador y administrador de contexto) y la clase MagicMock. En Python, el mocking se realiza principalmente con estos dos componentes potentes.
¿Qué NO queremos decir con mocking?
Los desarrolladores usan muchos objetos o módulos «mock» que son reemplazos locales completamente funcionales de servicios y API en red. Por ejemplo, la biblioteca moto es una biblioteca boto mock que captura todas las llamadas a la API de boto y las procesa localmente. Aunque estos mocks permiten probar API externas de forma local, aún requieren crear objetos reales. Este documento no trata ese tipo de mocking. En concreto, trata sobre el uso de objetos MagicMock para controlar por completo el flujo de ejecución de la función que se está probando, lo que facilita probar errores y el manejo de excepciones.
¿Cómo hacemos mocking en Python?
El mocking en Python se hace con patch, que intercepta una función de API o una llamada para crear un objeto. Cuando patch intercepta una llamada, devuelve un objeto MagicMock de forma predeterminada. Al definir propiedades en el objeto MagicMock, puedes hacer que la llamada a la API devuelva cualquier valor que quieras o que genere una Exception.
El procedimiento general es el siguiente:
Escribe la prueba como si usaras API externas reales.
En la función que estás probando, determina qué llamadas a API debes reemplazar por mocks; deberían ser pocas.
En la función de prueba, aplica patch a las llamadas a la API.
Configura las respuestas del objeto
MagicMock.Ejecuta la prueba.
Si la prueba pasa, terminaste. Si no, puede que haya un error en la función que estás probando o que hayas configurado incorrectamente la respuesta de MagicMock. A continuación, veremos con más detalle las herramientas que puedes usar para crear y configurar mocks.
patch
patch se puede usar como decorador de la función de prueba y recibe como argumento una cadena que indica el nombre de la función que se reemplazará. Para que patch encuentre la función que se reemplazará, debes especificar su nombre completo, que quizá no sea el que esperas. Si una clase se importa con una instrucción from module import ClassA, ClassA pasa a formar parte del espacio de nombres del módulo en el que se importa.
Por ejemplo, si se importa una clase en el módulo my_module.py de la siguiente manera:
Debes aplicar patch como @patch(my_module.ClassA), no como @patch(module.ClassA), debido a la semántica de la instrucción from ... import ..., que importa clases y funciones en el espacio de nombres actual.
Por lo general, se usa patch para reemplazar una llamada a una API externa o cualquier otra llamada a una función o creación de objeto que consuma mucho tiempo o recursos. Solo deberías aplicar patch a unos pocos objetos invocables por prueba. Si necesitas usar patch más de unas cuantas veces, considera refactorizar la prueba o la función que estás probando.
Al usar el decorador patch, se envía automáticamente un argumento posicional a la función que decoras (es decir, a tu función de prueba). Si aplicas patch a varias funciones, primero se ejecuta el decorador más cercano a la función decorada, por lo que este crea el primer argumento posicional.
De forma predeterminada, estos argumentos son instancias de MagicMock, el objeto de mocking predeterminado de unittest.mock. Puedes definir el comportamiento de la función con patch estableciendo atributos en la instancia MagicMock que se devuelve.
MagicMock
Los objetos MagicMock ofrecen una interfaz sencilla para crear mocks que te permite definir el valor de retorno u otro comportamiento de la función o llamada de creación de objetos a la que aplicaste patch. Así puedes definir por completo el comportamiento de la llamada y evitar crear objetos reales, lo que puede ser complicado. Por ejemplo, si aplicamos patch a una llamada a requests.get, una llamada a una biblioteca HTTP, podemos definir la respuesta que se devolverá cuando la función que estamos probando haga la llamada a la API, en lugar de asegurarnos de que haya un servidor de prueba disponible para devolver la respuesta deseada.
Los dos atributos más importantes de una instancia de MagicMock son return_value y side_effect; ambos permiten definir el comportamiento de retorno de la llamada a la que aplicaste patch.
return_value
El atributo return_value de la instancia MagicMock que se pasa a tu función de prueba te permite elegir qué devuelve el objeto invocable al que aplicaste patch. En la mayoría de los casos, querrás devolver una versión mock de lo que normalmente devolvería el objeto invocable. Puede ser JSON, un iterable, un valor, una instancia del objeto de respuesta real, un MagicMock que simula ser el objeto de respuesta o casi cualquier otra cosa. Al aplicar patch a objetos, la llamada reemplazada es la llamada de creación del objeto, por lo que el return_value de MagicMock debería ser un objeto mock, que podría ser otro MagicMock.
Si el código que estás probando sigue el estilo Pythonic y usa tipado pato en lugar de tipado explícito, usar un MagicMock como objeto de respuesta puede ser práctico. En vez de complicarte creando una instancia real de una clase, puedes definir pares arbitrarios de clave y valor de atributos en el constructor de MagicMock, y se aplicarán automáticamente a la instancia.
Ten en cuenta que el argumento que se pasa a test_some_func, es decir, mock_api_call, es un MagicMock y estamos configurando return_value para que sea otro MagicMock. Al hacer mocking, todo es un MagicMock.
Especificar una MagicMock
Aunque la flexibilidad de MagicMock resulta práctica para crear rápidamente mocks de clases con requisitos complejos, también puede ser una desventaja. De forma predeterminada, las instancias de MagicMock se comportan como si tuvieran cualquier atributo, incluso los que no quieres que tengan. En el ejemplo anterior, devolvemos un objeto MagicMock en lugar de un objeto Response. Pero imagina que nos equivocamos al aplicar patch y reemplazamos una función que debía devolver un objeto Request en vez de un objeto Response. El objeto MagicMock que devolvemos seguirá comportándose como si tuviera todos los atributos del objeto Request, aunque queríamos que simulara un objeto Response. Esto puede provocar errores confusos en las pruebas y comportamientos incorrectos.
La solución consiste en crear el MagicMock con spec, usando el argumento de palabra clave spec: MagicMock(spec=Response). Esto crea un MagicMock que solo permite acceder a los atributos y métodos de la clase especificada para MagicMock. Si intentas acceder a un atributo que no existe en el objeto original, se generará una AttributeError, tal como ocurriría con el objeto real.
Un ejemplo sencillo:
side_effect
A veces querrás probar que tu función maneja correctamente una excepción o que gestiona bien varias llamadas a la función a la que aplicaste patch. Puedes hacerlo con side_effect. Si asignas una excepción a side_effect, esa excepción se genera de inmediato cuando se llama a la función con patch.
Si asignas un iterable a side_effect, se devolverá el siguiente elemento del iterable cada vez que se llame a la función con patch. Si asignas cualquier otro valor a side_effect, se devolverá ese valor.
assert_called_with
assert_called_with comprueba que se haya llamado a la función con patch con los argumentos que se especifican como argumentos de assert_called_with.
Un ejemplo completo
En este ejemplo, pruebo una función retry en Client.update. Esto significa que las llamadas a API en update se harán dos veces, lo que es una excelente ocasión para usar MagicMock.side_effect.
Aquí está el código completo del ejemplo:
Estoy aplicando patch a dos llamadas en la función que estoy probando (pyvars.vars_client.VarsClient.update): una a VarsClient.get y otra a requests.post. Como aplico patch a dos llamadas, mi función de prueba recibe dos argumentos, a los que llamé mock_post y mock_get. Ambos son objetos MagicMock. En su estado predeterminado, no hacen mucho. Tenemos que asignarles algunos comportamientos de respuesta.
Esta prueba verifica que el mecanismo de reintento funcione correctamente, así que llamaré a update varias veces y haré varias llamadas a VarsClient.get y requests.post.
Here I set up the side_effects that I want. I want all the calls to VarsClient.get to work (returning an empty VarsResponse is fine for this test), the first call to requests.post to fail with an exception, and the second call to requests.post to work. This kind of fine-grained control over behavior is only possible through mocking.
Una vez que configuré los side_effects, el resto de la prueba es sencillo. El comportamiento es el siguiente: la primera llamada a requests.post falla, por lo que el mecanismo de reintento que envuelve VarsClient.update debería capturar el error y todo debería funcionar en el segundo intento. Podemos verificar aún más este comportamiento revisando el historial de llamadas de mock_get y mock_post.
Conclusión
Usar objetos mock de forma correcta va en contra de nuestra intuición de hacer pruebas lo más reales y exhaustivas posible, pero nos permite escribir pruebas independientes que se ejecutan rápidamente y sin dependencias. También nos permite probar el manejo de excepciones y los casos límite que, de otro modo, sería imposible probar. Y, lo más importante, nos da la libertad de enfocarnos en probar la funcionalidad de nuestro código, en lugar de nuestra capacidad para configurar un entorno de pruebas. Al concentrarnos en probar lo importante, podemos mejorar la cobertura de las pruebas y aumentar la confiabilidad de nuestro código, que es justamente la razón por la que hacemos pruebas.
Enlaces a la documentación
https://docs.python.org/3/library/unittest.mock.html
Seguridad de IaC diseñada para desarrolladores
Snyk protege tu infraestructura como código desde el ciclo de vida del desarrollo de software hasta el runtime en la nube con un motor unificado de políticas como código, para que todos los equipos puedan desarrollar, implementar y operar de forma segura.



