Pruebas unitarias en Python con Unittest

El código bien probado y documentado es fácil de mantener y más limpio para el equipo de desarrollo. En este artículo, nos centraremos en el marco Unittest y escribiremos pruebas unitarias para el código Python.

Introducción

En casi todos los campos, los productos se prueban exhaustivamente antes de lanzarlos al mercado para garantizar su calidad y que funcionen según lo previsto.

Los medicamentos, los productos cosméticos, los vehículos, los teléfonos y las computadoras portátiles se prueban para garantizar que mantengan un cierto nivel de calidad prometido al consumidor. Dada la influencia y el alcance del software en nuestra vida diaria, es importante que lo probemos a fondo antes de lanzarlo a nuestros usuarios para evitar que surjan problemas cuando esté en uso.

Hay varias formas y métodos de probar nuestro software, y en este artículo nos concentraremos en probar nuestros programas de Python usando el framework Prueba de unidad .

Pruebas unitarias frente a otras formas de prueba

Hay varias formas de probar el software que se agrupan principalmente en pruebas funcionales y no funcionales.

  • Pruebas no funcionales: destinadas a verificar y verificar los aspectos no funcionales del software, como confiabilidad, seguridad, disponibilidad y escalabilidad. Los ejemplos de pruebas no funcionales incluyen pruebas de carga y pruebas de estrés.
  • Pruebas funcionales: Implica probar nuestro software contra los requisitos funcionales para garantizar que ofrece la funcionalidad requerida. Por ejemplo, podemos probar si nuestra plataforma de compras envía correos electrónicos a los usuarios después de realizar sus pedidos simulando ese escenario y verificando el correo electrónico.

Las pruebas unitarias se incluyen en las pruebas funcionales junto con las pruebas de integración y las pruebas de regresión.

La prueba unitaria se refiere a un método de prueba en el que el software se divide en diferentes componentes (unidades) y cada unidad se prueba funcionalmente y de forma aislada de las otras unidades o módulos.

Una unidad aquí se refiere a la parte más pequeña de un sistema que logra una sola función y es comprobable. El objetivo de las pruebas unitarias es verificar que cada componente de un sistema funcione como se espera, lo que a su vez confirma que todo el sistema cumple y cumple con los requisitos funcionales.

Las pruebas unitarias generalmente se realizan antes de las pruebas de integración ya que, para verificar que las partes de un sistema funcionan bien juntas, primero debemos verificar que funcionan como se espera individualmente. Por lo general, también lo llevan a cabo los desarrolladores que construyen los componentes individuales durante el proceso de desarrollo.

Beneficios de las pruebas unitarias

Las pruebas unitarias son beneficiosas porque solucionan errores y problemas al principio del proceso de desarrollo y, finalmente, lo aceleran.

El costo de corregir los errores identificados durante las pruebas unitarias también es bajo en comparación con corregirlos durante las pruebas de integración o durante la producción.

Las pruebas unitarias también sirven como documentación del proyecto al definir qué hace cada parte del sistema a través de pruebas bien escritas y documentadas. Al refactorizar un sistema o agregar funciones, las pruebas unitarias ayudan a protegerse contra los cambios que rompen la funcionalidad existente.

Marco de pruebas unitarias {#marco de pruebas unitarias}

Inspirado en el Marco de pruebas JUnit para Java, unittest es un marco de prueba para programas de Python que viene incluido con la distribución de Python desde Python 2.1. A veces se denomina PyUnit. El marco admite la automatización y la agregación de pruebas y el código de configuración y apagado común para ellas.

Logra esto y más a través de los siguientes conceptos:

  • Accesorio de prueba: Define la preparación requerida para la ejecución de las pruebas y cualquier acción que se deba realizar después de la conclusión de una prueba. Los accesorios pueden incluir la configuración y conexión de la base de datos, la creación de archivos o directorios temporales y la posterior limpieza o eliminación de los archivos después de que se haya completado la prueba.
  • Caso de prueba: se refiere a la prueba individual que verifica una respuesta específica en un escenario dado con entradas específicas.
  • Test Suite: Representa una agregación de casos de prueba que están relacionados y deben ejecutarse juntos.
  • Test Runner: coordina la ejecución de las pruebas y proporciona los resultados del proceso de prueba al usuario a través de una interfaz gráfica de usuario, la terminal o un informe escrito en un archivo.

unittest no es el único marco de prueba para Python que existe, otros incluyen Pytest, [Marco de trabajo de robots](https://robotframework. org/#/), Lechuga para BDD, y Marco de comportamiento.

If you're interested in reading more about Desarrollo basado en pruebas en Python con PyTest, we've got you covered!

Marco Unittest en acción

Vamos a explorar el marco unittest construyendo una aplicación de calculadora simple y escribiendo las pruebas para verificar que funciona como se espera. Usaremos el proceso de Desarrollo basado en pruebas comenzando con las pruebas y luego implementando la funcionalidad para que pasen las pruebas.

Aunque es una buena práctica desarrollar nuestra aplicación Python en un entorno virtual, para este ejemplo no será obligatorio ya que unittest se envía con la distribución de Python y no necesitaremos ningún otro paquete externo para construir nuestra calculadora.

Nuestra calculadora realizará operaciones simples de suma, resta, multiplicación y división entre dos números enteros. Estos requisitos guiarán nuestras pruebas funcionales utilizando el marco unittest.

Probaremos las cuatro operaciones admitidas por nuestra calculadora por separado y escribiremos las pruebas para cada una en un conjunto de pruebas separado, ya que se espera que las pruebas de una operación en particular se ejecuten juntas. Nuestras suites de prueba se alojarán en un archivo y nuestra calculadora en un archivo separado.

Nuestra calculadora será una clase SimpleCalculator con funciones para manejar las cuatro operaciones que se esperan de ella. Comencemos a probar escribiendo las pruebas para la operación de suma en nuestro test_simple_calculator.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import unittest
from simple_calculator import SimpleCalculator

class AdditionTestSuite(unittest.TestCase):
    def setUp(self):
        """ Executed before every test case """
        self.calculator = SimpleCalculator()

    def tearDown(self):
        """ Executed after every test case """
        print("\ntearDown executing after the test case. Result:")

    def test_addition_two_integers(self):
        result = self.calculator.sum(5, 6)
        self.assertEqual(result, 11)

    def test_addition_integer_string(self):
        result = self.calculator.sum(5, "6")
        self.assertEqual(result, "ERROR")

    def test_addition_negative_integers(self):
        result = self.calculator.sum(-5, -6)
        self.assertEqual(result, -11)
        self.assertNotEqual(result, 11)

# Execute all the tests when the file is executed
if __name__ == "__main__":
    unittest.main()

Comenzamos importando el módulo unittest y creando un conjunto de pruebas (AdditionTestSuite) para la operación de adición.

En él, creamos un método setUp() que se llama antes de cada caso de prueba para crear nuestro objeto SimpleCalculator que se usará para realizar los cálculos.

El método tearDown() se ejecuta después de cada caso de prueba y dado que no tenemos mucho uso para él en este momento, solo lo usaremos para imprimir los resultados de cada prueba.

Las funciones test_addition_two_integers(), test_addition_integer_string() y test_addition_negative_integers() son nuestros casos de prueba. Se espera que la calculadora sume dos enteros positivos o negativos y devuelva la suma. Cuando se le presenta un número entero y una cadena, se supone que nuestra calculadora devolverá un error.

assertEqual() y assertNotEqual() son funciones que se utilizan para validar la salida de nuestra calculadora. La función assertEqual() verifica si los dos valores proporcionados son iguales, en nuestro caso, esperamos que la suma de 5 y 6 sea 11, por lo que compararemos esto con el valor devuelto por nuestra calculadora .

Si los dos valores son iguales, la prueba ha pasado. Otras funciones de aserción que ofrece unittest incluyen:

  • assertTrue(a): comprueba si la expresión proporcionada es verdadera
  • assertGreater(a, b): Comprueba si a es mayor que b
  • assertNotIn(a, b): comprueba si a está en b
  • assertLessEqual(a, b): comprueba si a es menor o igual que b
  • etc...

Una lista de estas afirmaciones se puede encontrar en esta hoja de trucos.

Cuando ejecutamos el archivo de prueba, esta es la salida:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ python3 test_simple_calulator.py

tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
tearDown executing after the test case. Result:
E
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 22, in test_addition_integer_string
    result = self.calculator.sum(5, "6")
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

======================================================================
ERROR: test_addition_negative_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 26, in test_addition_negative_integers
    result = self.calculator.sum(-5, -6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

======================================================================
ERROR: test_addition_two_integers (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 18, in test_addition_two_integers
    result = self.calculator.sum(5, 6)
AttributeError: 'SimpleCalculator' object has no attribute 'sum'

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (errors=3)

En la parte superior de la salida, podemos ver la ejecución de la función tearDown() a través de la impresión del mensaje que especificamos. A esto le sigue la letra E y mensajes de error derivados de la ejecución de nuestras pruebas.

Hay tres posibles resultados de una prueba, puede pasar, fallar o encontrar un error. El marco unittest indica los tres escenarios mediante el uso de:

  • Un punto (.): Indica una prueba de aprobación
  • La letra ‘F’: Indica una prueba fallida
  • La letra ‘E’: Indica que ocurrió un error durante la ejecución de la prueba

En nuestro caso, estamos viendo la letra E, lo que significa que nuestras pruebas encontraron errores que ocurrieron al ejecutar nuestras pruebas. Estamos recibiendo errores porque aún no hemos implementado la funcionalidad suma de nuestra calculadora:

1
2
3
4
class SimpleCalculator:
    def sum(self, a, b):
        """ Function to add two integers """
        return a + b

Nuestra calculadora ahora está lista para agregar dos números, pero para asegurarnos de que funcionará como se espera, eliminemos la función tearDown() de nuestras pruebas y ejecutemos nuestras pruebas una vez más:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ python3 test_simple_calulator.py
E..
======================================================================
ERROR: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 22, in test_addition_integer_string
    result = self.calculator.sum(5, "6")
  File "/Users/robley/Desktop/code/python/unittest_demo/src/simple_calculator.py", line 7, in sum
    return a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (errors=1)

Nuestros errores se han reducido de 3 a solo 1. El resumen del informe en la primera línea E.. indica que una prueba resultó en un error y no pudo completar la ejecución, y las dos restantes pasaron. Para que la primera prueba pase, tenemos que refactorizar nuestra función de suma de la siguiente manera:

1
2
3
    def sum(self, a, b):
        if isinstance(a, int) and isinstance(b, int):
            return a + b

Cuando ejecutamos nuestras pruebas una vez más:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ python3 test_simple_calulator.py
F..
======================================================================
FAIL: test_addition_integer_string (__main__.AdditionTestSuite)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_simple_calulator.py", line 23, in test_addition_integer_string
    self.assertEqual(result, "ERROR")
AssertionError: None != 'ERROR'

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

Esta vez, nuestra función de suma se ejecuta hasta el final, pero nuestra prueba falla. Esto se debe a que no devolvimos ningún valor cuando una de las entradas no es un número entero. Nuestra afirmación compara Ninguno con ERROR y dado que no son iguales, la prueba falla. Para que nuestra prueba pase tenemos que devolver el error en nuestra función sum():

1
2
3
4
5
def sum(self, a, b):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    else:
        return "ERROR"

Y cuando ejecutamos nuestras pruebas:

1
2
3
4
5
6
$ python3 test_simple_calulator.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Todas nuestras pruebas pasan ahora y obtenemos 3 puntos completos para indicar que todas nuestras 3 pruebas para la funcionalidad de adición están pasando. Los conjuntos de pruebas de resta, multiplicación y división también se implementan de manera similar.

También podemos probar si se genera una excepción. Por ejemplo, cuando un número se divide por cero, se genera la excepción ZeroDivisionError. En nuestro DivisionTestSuite, podemos confirmar si se generó la excepción:

1
2
3
4
5
6
7
8
class DivisionTestSuite(unittest.TestCase):
    def setUp(self):
        """ Executed before every test case """
        self.calculator = SimpleCalculator()

    def test_divide_by_zero_exception(self):
        with self.assertRaises(ZeroDivisionError):
            self.calculator.divide(10, 0)

test_divide_by_zero_exception() ejecutará la función divide(10, 0) de nuestra calculadora y confirmará que la excepción efectivamente se generó. Podemos ejecutar DivisionTestSuite de forma aislada, de la siguiente manera:

1
2
3
4
5
6
$ python3 -m unittest test_simple_calulator.DivisionTestSuite.test_divide_by_zero_exception
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

El conjunto completo de pruebas de la funcionalidad de división se puede encontrar en el vínculo esencial a continuación junto con los conjuntos de pruebas para la funcionalidad de multiplicación y resta.

Conclusión

En este artículo, hemos explorado el marco unittest e identificado las situaciones en las que se usa al desarrollar programas de Python. El marco unittest, también conocido como PyUnit, viene con la distribución de Python de forma predeterminada a diferencia de otros marcos de prueba. De manera TDD, escribimos las pruebas para una calculadora simple, ejecutamos las pruebas y luego implementamos la funcionalidad para hacer que las pruebas pasen.

El marco unittest proporcionó la funcionalidad para crear y agrupar casos de prueba y comparar la salida de nuestra calculadora con la salida esperada para verificar que funciona como se esperaba.

La calculadora completa y los conjuntos de pruebas se pueden encontrar aquí en esta esencia en GitHub.