Parche simulado de una función invocada por otra función en Python

Mehvish Ashiq 21 junio 2023
  1. Objetos simulados y su importancia
  2. Python Patch() y sus usos
Parche simulado de una función invocada por otra función en Python

Las pruebas unitarias son esenciales cuando escribimos código robusto. Nos ayudan a verificar la lógica de nuestra aplicación e inspeccionar todos los demás aspectos de los requisitos del código; sin embargo, obstáculos como la complejidad y las dependencias externas dificultan la escritura de casos de prueba de calidad.

En este punto, unittest.mock, una biblioteca simulada de Python, nos ayuda a superar estos obstáculos. Hoy, aprenderemos sobre los objetos simulados, su importancia y varios códigos de ejemplo usando Python patch(), que reemplazará temporalmente el objetivo con los objetos simulados.

Objetos simulados y su importancia

Los objetos simulados imitan el comportamiento de los objetos reales de manera controlada, por ejemplo, dentro del entorno de prueba, donde realizamos diferentes pruebas para garantizar que el código funcione como se espera. El proceso de usar objetos simulados se llama Mocking; es una poderosa herramienta para mejorar la calidad de nuestras pruebas.

Ahora, el punto es, ¿por qué tenemos que pasar por la burla? ¿Cuáles son las razones fundamentales para usarlo?

Varias causas muestran la importancia de usar burlas en Python. Echémosles un vistazo a continuación:

  1. Una razón principal es mejorar el comportamiento de nuestro código mediante el uso de objetos ficticios. Por ejemplo, podemos usarlos para probar solicitudes HTTP (una solicitud realizada por un cliente al host designado, que se ubica en un servidor) y asegurarnos de que causen una falla intermitente o no.
  2. Podemos reemplazar las solicitudes HTTP reales con los objetos simulados, lo que nos permite simular una interrupción del servicio externo y respuestas completamente exitosas de una manera predecible.
  3. La burla también es útil cuando es difícil probar las declaraciones if y los bloques except. Usando objetos simulados, podemos controlar el flujo de ejecución de nuestro código para llegar a dichas áreas (if y except) y mejorar la cobertura del código.
  4. Otra razón que aumenta la importancia de usar objetos simulados de Python es comprender correctamente cómo podemos usar sus contrapartes en nuestro código.

Python Patch() y sus usos

El unittest.mock, una biblioteca de objetos simulados en Python, tiene un parche() que reemplaza temporalmente el objetivo con el objeto simulado. Aquí, el objetivo puede ser una clase, un método o una función.

Para usar el parche, necesitamos entender cómo identificar un objetivo y llamar a la función patch().

Para reconocer el objetivo, asegúrese de que el objetivo sea importable, luego parchee el objetivo donde se utiliza, no de donde proviene. Podemos llamar a patch() de tres formas; como decorador de una clase/función, como administrador de contexto o como inicio/detención manual.

El objetivo se reemplaza con el nuevo objeto cuando usamos patch() como decorador de una clase/función o usamos patch() en el administrador de contexto dentro de una declaración with. En ambos escenarios, el parche se deshace cuando existe la instrucción with o la función.

Vamos a crear un código de inicio y guardarlo en el archivo addition.py, que importaremos para usar patch() como decorador, administrador de contexto e inicio/parada manual.

Código de ejemplo de inicio (guardado en el archivo addition.py):

def read_file(filename):
    with open(filename) as file:
        lines = file.readlines()
        return [float(line.strip()) for line in lines]


def calculate_sum(filename):
    numbers = read_file(filename)
    return sum(numbers)


filename = "./test.txt"
calculate_sum(filename)

Contenido de test.txt:

1
2
3

read_file() toma filename para leer líneas y convierte cada línea en tipo flotante. Devuelve una lista de estos números convertidos, que guardamos en la variable numbers dentro de la función calculate_sum().

Ahora, calculate_sum() suma todos los números en la lista de números y los devuelve como una salida de la siguiente manera:

Producción :

6.0

Usa patch() como decorador

Profundicemos paso a paso para aprender el uso de patch() como decorador. El código fuente completo se proporciona al final de todos los pasos.

  • Importación de bibliotecas y módulos.
    import unittest
    from unittest.mock import patch
    import addition
    

    Primero, importamos la biblioteca unittest, luego patch desde el módulo unittest.mock. Después de eso, importamos adición, nuestro código de inicio.

  • Decora el método test_calculate_sum().
    @patch('addition.read_file')
    def test_calculate_sum(self, mock_read_file):
        # ....
    

    A continuación, decoramos el método de prueba test_calculate_sum() utilizando el decorador @patch. Aquí, el objetivo es la función read_file() del módulo addition.

    El test_calculate_sum() tiene un parámetro adicional mock_read_file debido al uso del decorador @patch. Este parámetro adicional es una instancia de MagicMock (podemos cambiar el nombre de mock_read_file a cualquier cosa que queramos).

    Dentro de test_calculate_sum(), el patch() reemplaza la función addition.read_file() con el objeto mock_read_file.

  • Asigne una lista a mock_read_file.return_value.
    mock_read_file.return_value = [1, 2, 3]
    
  • Llame a calculate_sum() y pruébelo.
    result = addition.calculate_sum("")
    self.assertEqual(result, 6.0)
    

    Ahora, podemos llamar a calculate_sum() y usar assertEqual() para probar si la suma es 6.0 o no.

    Aquí, podemos pasar cualquier nombre de archivo a la función calculate_sum() porque se invocará el objeto mock_read_file en lugar de la función addition.read_file().

  • Aquí está el código fuente completo.

    Código de ejemplo (guardado en el archivo test_sum_patch_decorator.py)

    import unittest
    from unittest.mock import patch
    import addition
    
    
    class TestSum(unittest.TestCase):
        @patch("addition.read_file")
        def test_calculate_sum(self, mock_read_file):
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6.0)
    

    Ejecutar una prueba:

    python -m unittest test_sum_patch_decorator -v
    

    Ejecute la prueba usando el comando anterior para obtener el siguiente resultado.

    PRODUCCIÓN:

    test_calculate_sum (test_sum_patch_decorator.TestSum) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK
    

Use patch() como administrador de contexto

Código de ejemplo (guardado en el archivo test_sum_patch_context_manager.py):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        with patch("addition.read_file") as mock_read_file:
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum("")
            self.assertEqual(result, 6)

Este código es similar al último ejemplo de código donde usamos patch() como decorador, excepto por algunas diferencias discutidas aquí.

Ahora, no tenemos la línea de código @patch('addition.read_file'), mientras que test_calculate_sum() solo toma el parámetro self (que es un parámetro predeterminado).

El with patch('addition.read_file') como mock_read_file significa parchear addition.read_file() utilizando el objeto mock_read_file en el administrador de contexto.

En palabras simples, podemos decir que el patch() reemplazará el objeto addition.read_file() con el objeto mock_read_file dentro del bloque with.

Ahora, ejecute la prueba usando el siguiente comando.

python -m unittest test_sum_patch_context_manager -v

Producción :

test_calculate_sum (test_sum_patch_context_manager.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

Utilice patch() para iniciar/detener manualmente

Código de ejemplo (guardado en el archivo test_sum_patch_manually.py):

import unittest
from unittest.mock import patch
import addition


class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        patcher = patch("addition.read_file")
        mock_read_file = patcher.start()
        mock_read_file.return_value = [1, 2, 3]
        result = addition.calculate_sum("")
        self.assertEqual(result, 6.0)
        patcher.stop()

Esta valla de código hace lo mismo que los dos ejemplos de código anteriores, pero aquí usamos patch() manualmente. ¿Cómo? Entendámoslo a continuación.

Primero, importamos las bibliotecas requeridas. Dentro de test_calculate_sum(), llamamos patch() para iniciar un parche con la función objetivo read_file() del módulo addition.

Luego, creamos un objeto simulado para la función read_file(). Después de eso, asigne la lista de números a mock_read_file.return_value, llame a calculate_sum() y pruebe los resultados usando assertEqual().

Finalmente, dejamos de aplicar parches llamando al método stop() del objeto patcher. Ahora, ejecute el siguiente comando para probar.

python -m unittest test_sum_patch_manually -v

Producción :

test_calculate_sum (test_sum_patch_manually.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Mehvish Ashiq avatar Mehvish Ashiq avatar

Mehvish Ashiq is a former Java Programmer and a Data Science enthusiast who leverages her expertise to help others to learn and grow by creating interesting, useful, and reader-friendly content in Computer Programming, Data Science, and Technology.

LinkedIn GitHub Facebook