Cómo usar funciones como Mocks para tests en Python
Una de las preguntas frecuentes sobre el Testing es: ¿cómo evito llamar a servicios “pesados” durante la ejecución de pruebas?
Si tuviera que llamar a servicios como Redis, Docker, MySQL, RabbitMQ u otros por cada prueba, probablemente tendría que instalarlos en mi servidor de pruebas, lo cual consumiría recursos y aumentaría el tiempo de ejecución. No creo que al resto de los desarrolladores les guste esa idea.
Además, es útil para evitar llamar direcciones URL reales. Ya que no tenemos control sobre esas direcciones, podrían estar disponibles o no en cualquier momento del día.
Podemos aprovechar los Mocks en Python para verificar, por ejemplo, que nuestro código realiza una llamada a una dirección URL externa y forzar la ejecución de casos alternos. Como cuando el servicio no está disponible o cuando devuelve una respuesta no exitosa.
Para el proyecto de ejemplo que mostraré, utilizaré Poetry como gestor de paquetes, la biblioteca requests
para llamadas a una URL externa y pytest
con unittest
como bibliotecas de testing.
Los siguientes ejemplos de código han sido probados en Python 3.11
Usando Poetry para configurar el proyecto
Poetry es un administrador de paquetes muy fácil de usar. Puedes descargarlo en https://python-poetry.org/ . Acá el listado de dependencias a utilizar.
Tenemos: black
, un formateador de código, pytest
y pytest-mock
para tener una manera sencilla de realizar mocks en Python, estarán dentro de las dependencias de desarrollo. No las necesitamos en nuestro código desplegado a producción.
Como dependencias de producción usaremos python 3.11
y requests
, la librería para hacer llamadas HTTP a cualquier URL.
Creando un script Python para llamar una API
Como cualquier script en Python, realizaremos algo muy sencillo. Procederé a realizar los 3 tipos de llamadas API más comunes: una llamada GET, una llamada POST y una llamada POST con datos.
Si ejecuto este script, funcionará de todas maneras mientras la URL de JsonPlaceholder se encuentre disponible.
Usando mocks para sobrescribir el comportamiento de la función
Vamos a usar frecuentemente el método patch
de unittest.mock
. Con este decorador, podremos hacer un parche del método que queramos y convertirlo en un mock. En el proceso adquirirá varias funciones útiles que nos permitirán saber si fue llamado, con qué parámetros e incluso colocar un valor falso de retorno si queremos.
La función a la cual debemos hacer un mock en este caso es requests.get
. Asegúrate de importar solo get
desde requests
. Debemos poner la dirección completa de la función, incluyendo el paquete.
Las funciones dentro de los mocks son tan diversas que algunas veces puedes usar assert
o ya los assertions vienen dentro de los mocks.
Comprobar si la función fue llamada sólo una vez, modo decorador
Para este caso, usaremos el keyword especial de Python assert
. El mock viene con una función que retorna verdadero o falso, llamado called_once
.
Observa como usamos un decorador, arriba de nuestro test test_call_single_endpoint_mock
. De este modo se evita sangrar el código.
Comprobar si la función fue llamada sólo una vez, usando with
Podemos además no hacer uso del decorador, en este caso no necesitaríamos patch. Es una línea extra con sangrado que tenemos que añadir. Personalmente, prefiero la versión con decorador, pero nunca está de más aprender las maneras alternativas.
Comprobar si la función fue llamada, usando el parámetro mocker
Puedes además usar mocker
como parámetro para poder hacer un mock de la función que deseas sobrescribir. Con este método también evitamos un sangrado extra. La ventaja es que con este parámetro puedes hacer un mock de más funciones así evitas acumular decoradores.
Comprobar si la función fue llamada con parámetros específicos
Tenemos dos versiones de estos assertions. Podemos verificar si una función fue llamada con parámetros específicos. Para eso usamos la función del mock assert_called_with
. Hay una variación de esta, la cual se asegura de que fue llamada una sola vez exactamente: assert_called_once_with
.
Agrupando tests
Si queremos tener un mejor orden para nuestros tests, podemos agruparlos dentro de una clase. Incluso puedes combinar tests agrupados y no agrupados dentro de un mismo archivo.
Para el siguiente ejemplo, le diremos al mock el objeto a retornar. Esto puede ser útil si después quieres forzar un flujo específico para comprobar cómo se comportan tus funciones a las que haces mock.
Recuerda que dentro de una clase, el primer parámetro de sus funciones siempre será self
. Así que el segundo parámetro será tu función como mock.
Comprobar llamadas consecutivas de un método
Dentro de la función como mock, viene un método llamado side_effect
. Útil cuando tu método es llamado más de una vez y quieres programar retornos consecutivos.
El método side_effect
también es usado para programar Exceptions como respuesta.
Comprobar que una llamada fue similar usando call
Si no queremos usar assert_called_with
o assert_called_once_with
, podemos también usar la función que viene dentro de unittest
llamada call
. Lo que hace call, es retornar una llamada a un Mock
o MagickMock
. Así se podrá comparar si las llamadas en algún punto del test son iguales a lo que el mock ha registrado.
Comprobar que el parámetro n de una función llamada fue un valor específico
Esto es una variación de un test en el que solo queremos saber si el parámetro en una posición específica de una función tiene un valor específico. Dado que podemos obtener los parámetros de un mock como una lista, será fácil acceder a un índice específico.
En este ejemplo, podemos obtener todos los parámetros de una llamada a una función dentro de call_args
.
Resultado final
Este es código final de todos los tests que he realizado. No cubre el 100% de situaciones que una aplicación podría tener, pero espero haber mostrado los casos más usados.
El enlace al repositorio estará en la sección de Recomendaciones.
Recomendaciones
Puedes ver el repositorio con el resultado final acá .
Para más información de unittest.mock
consulta la documentación .