Realizar testing de software no es una tarea simple: las funciones, métodos, objetos y diferentes componentes que forman el software que desarrollamos interactúan entre sí la mayor parte de las veces, pero es fácil probar algo de forma aislada. Pero si lo que queremos testear está relacionado con funciones, métodos o servicios externos, la cosa se pone un poco más compleja. En estos casos, hay una mayor probabilidad de que las pruebas fallen. Pero si el objetivo a la hora de escribir tests es poder probar los componentes de forma aislada, ¿cómo lo hacemos si depende del resultado de una petición HTTP o de otro método o función? La clave está en desacoplar lo que queremos probar usando mocks.

Un mock es un objeto dummy, un objeto falso que devuelve lo que nosotros queremos o necesitamos para usar dentro del test. Normalmente se mockean librerías de terceros o llamadas a APIs, que no necesitamos o no forman parte del test que estamos realizando. De esta forma podemos ejecutar nuestros tests sin la necesidad de hacer peticiones HTTP innecesarias, lo cual nos produce un ahorro de tiempo y recursos. También podemos aplicar mocks cuando algunas funcionalidades dependen de otras que todavía no han sido desarrolladas.

Como mencionamos en algunos casos, no es posible probar con datos reales ya que pueden no estar disponibles en un entorno de desarrollo. Además, consume un mayor tiempo en la ejecución, y lo que buscamos es poder correr los tests de manera rápida y en repetidas ocasiones

Librería mock de Python

Existen varias formas de realizar mockeos utilizando la librería mock de Python, ya que nos permite reemplazar las partes de nuestra aplicación que queremos testear con objetos simulados. Por ejemplo, en el caso de que queramos devolver un valor específico podemos hacerlo usando return_value igual al valor que queremos devolver.

Para nuestro ejemplo tenemos un archivo llamado some_function.py, donde declaramos dos funciones: una llamada get_greeting, que devuelve un string “Hola mundo”; y la segunda función llamada hello, la cual llama a get_greeting() .


def hello():
   return get_greeting()

def get_greeting():
   return "Hola Mundo"

Creamos un archivo llamado test_one.py dentro de la carpeta tests:

from unittest import TestCase, mock
from some_function import hello

class TestMockValue(TestCase):
   @mock.patch("some_function.get_greeting")
   def test_get_text(self, mock_response):
       mock_response.return_value = "texto mockeado"
       response = hello()
       self.assertEqual(response, "texto mockeado")

Utilizando patch mockeamos la función get_greeting. Aquí, patch recibe como parámetro la ruta de la función que queremos mockear y nuestro test recibe como parámetro un alias que hace referencia a lo que estamos mockeando.

 mock_response.return_value = "texto mockeado"

En la línea anterior se produce el mock de la función. Ahora, cada vez que llamemos a la función :“hello”_ dentro de nuestro test, en lugar de retornarnos “Hola mundo” como está en su definición, nos devolverá “texto mockeado”.

Dentro de lo que son los patchs también podemos hacer uso de patch objects, utilizado para mockear métodos dentro de una clase:

@mock.patch.object(Calculadora, "suma")
def test_calculadora(self, mock_method):
   mock_method.return_value = 10
   calculadora = Calculadora()
   result = calculadora.suma(5, 2)
   self.assertEqual(10, result)

El método suma() que está en la clase Calculadora en el código que acabamos de mostrar se reemplaza por un mock, y se le asignará el valor de 10 independientemente de los valores que pasemos como parámetro.

Otro caso aparece cuando queremos devolver una excepción: simplemente usamos side_effect y ponemos la excepción que esperamos. Este caso también nos sirve si queremos mockear una respuesta; por ejemplo, un status code.

También podemos hacer mock de las peticiones HTTP. Como comentamos en un principio, siempre es mejor desacoplar la dependencia de lo que queremos testear. De esta forma ahorramos recursos y tiempo.

Para nuestro ejemplo tenemos un archivo posts.py, que tendrá el siguiente código:

import requests

POSTS_URL = "https://jsonplaceholder.typicode.com/posts"

def get_posts():
   response = requests.get(POSTS_URL)
   if response.ok:
       return response
   else:
       return None

Importamos la librería requests del core de Python y creamos una función llamada get_posts, que hará un get a esta url y en caso de que la respuesta que vuelva sea ok, devolvemos el response.

En nuestra carpeta test:

from unittest import TestCase
from posts import get_posts

class TestGetPost(TestCase):
   def test_blog_posts(self):
       response = get_posts()
       self.assertEqual(response.status_code, 200)

Importamos TestCase de unittest y la función get_posts de posts.py. Creamos la clase _TestGetPost: que va a heredar de TestCase y, dentro de ella, vamos a declarar nuestro test test_blog_posts, donde hacemos uso de la función get_posts declarada previamente.

Al ejecutar el test con el comando python -m unittest:

Vemos que el test corre sin ningún problema, pero cada vez que se ejecuta hace una petición HTTP. Esto no está bien. En el caso de que tengamos varios tests que hagan peticiones HTTP a otras APIs, incurrirá en más tiempo y recursos para que todos nuestros tests se ejecuten por completo. Al ser API de terceros no es algo que nosotros debamos mantener ni testear, es por ello que podríamos mockear la respuesta de la API y de este modo solo centrarnos en nuestro código, en nuestro caso la función get_posts.

Para mockear la respuesta de la petición HTTP que estamos haciendo en nuestro test utilizaremos patch, tal como vimos en el primer ejemplo, mockeando get_greeting. Esto simplemente reemplaza la funcionalidad o lo que se va a ejecutar en esta función o método, y cada vez que sea llamado devolverá lo que nosotros deseemos.

Refactorizando un poco, nuestro test quedaría de la siguiente forma:

from unittest import TestCase, mock
from posts import get_posts

class TestGetPost(TestCase):
   @mock.patch("requests.get")  # Mock requests module get
   def test_blog_posts(self, mock_get):
       expected = [
           {
               "userId": 1,
               "id": 1,
               "title": "Test Title",
               "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam viverra convallis ex, et vehicula nulla ultrices.",
           },
           {
               "userId": 1,
               "id": 2,
               "title": "Test Title 2",
               "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam viverra convallis ex, et vehicula nulla ultrices.",
           }
       ]

       mock_get.return_value.status_code = 200
       mock_get.return_value.json.return_value = expected
       response = get_posts()

       # Assert that the request-response cycle completed successfully.
       self.assertEqual(response.status_code, 200)
       self.assertEqual(response.json(), expected)

En este caso, el mock se hará en la librería requests del core de Python, específicamente en el método GET; es decir, al hacer un GET devolverá lo que nosotros especifiquemos para este test.

Para realizar el mock utilizamos el decorador patch y pasamos como parámetro un string con la ruta a la funcionalidad que queremos mockear. El test ahora recibe un parámetro adicional mock_get, que es un alias con el que vamos a hacer referencia al objeto moqueado.

Declaramos un array con la información que esperamos que devuelva la función una vez que realiza el requests.get y a nuestro objeto mock_get le asignamos status_code = 200 y, además, el resultado que esperamos.

¿Qué sucederá ahora cuando llamamos a get_posts()? En lugar de realizar la petición HTTP al endpoint de posts, la respuesta y el status_code se mockea. De esta forma nos ahorramos hacer una llamada innecesaria, puesto que ya sabemos de antemano lo que nos va a devolver.

Al correr nuevamente el test vemos como el tiempo de ejecución es menor, ya que no tiene que hacer la petición HTTP. Baja de 0.072s a 0.001s.

Conclusiones

Existen muchas librerías que nos facilitan el trabajo con los tests que no forman parte del core de Python como unittest.mock. Una de ellas es requests-mock que nos facilitan aún más la vida, con la que podemos realizar todos los mocks de peticiones HTTP y devolver en cada test respuestas precargadas.

Utilizar mock a la hora de realizar tests en Python es una buena práctica, ya que en muchas ocasiones no podremos probar con datos reales al no estar disponibles en la ejecución del test y, como pudimos ver en el ejemplo, es algo bastante simple de hacer. Nos acorta el tiempo de ejecución de los tests y ahorramos recursos. Por otro lado y como mencionamos anteriormente, es más sencillo probar algo de forma aislada y totalmente desacoplado.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.