Pruebas unitarias en Python con Unittest

P

Introducción

En casi todos los campos, los productos se prueban minuciosamente antes de ser lanzados al mercado para garantizar su calidad y que funcionan 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 que se prometió al consumidor. Dada la influencia y el alcance del software en nuestra vida diaria, es importante que probemos nuestro software minuciosamente 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 Python usando el Prueba de unidad marco de referencia.

Prueba unitaria frente a otras formas de prueba

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

  • Pruebas no funcionales: Destinado a verificar y comprobar los aspectos no funcionales del software, como la fiabilidad, la seguridad, la disponibilidad y la escalabilidad. Ejemplos de pruebas no funcionales incluyen pruebas de carga y pruebas de estrés.
  • Pruebas funcionales: Implica probar nuestro software con 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.

Las pruebas unitarias se refieren 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.

Aquí, una unidad se refiere a la parte más pequeña de un sistema que logra una única función y se puede probar. El objetivo de las pruebas unitarias es verificar que cada componente de un sistema funcione según lo esperado, lo que a su vez confirma que todo el sistema cumple y cumple 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 funcionen como se espera individualmente. Generalmente 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 corrigen errores y problemas al principio del proceso de desarrollo y, finalmente, lo acelera.

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 lo que 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 cambios que rompen la funcionalidad existente.

Marco de prueba unitaria

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 lo denomina PyUnit. El marco admite la automatización y agregación de pruebas y el código común de configuración y apagado para ellas.

Lo logra y más a través de los siguientes conceptos:

  • Accesorio de prueba: Define la preparación necesaria para la ejecución de las pruebas y las acciones que deben realizarse 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 limpieza o eliminación posterior de los archivos después de que se haya completado la prueba.
  • Caso de prueba: Se refiere a la prueba individual que busca una respuesta específica en un escenario dado con entradas específicas.
  • Banco de pruebas: Representa una agregación de casos de prueba que están relacionados y deben ejecutarse juntos.
  • Corredor de pruebas: 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, el 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 robot, Lechuga para BDD, y Behave Framework.

Si está interesado en leer más sobre el desarrollo basado en pruebas en Python con PyTest, ¡lo tenemos cubierto!

Marco Unittest en acción

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

Si bien 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 unittest marco de referencia.

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á un SimpleCalculator clase 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:

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()

Empezamos importando el unittest módulo y creando una suite de pruebas (AdditionTestSuite) para la operación de adición.

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

los tearDown() El método se ejecuta después de cada caso de prueba y, dado que no lo utilizamos mucho 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 números enteros positivos o negativos y devuelva la suma. Cuando se le presenta un número entero y una cadena, se supone que nuestra calculadora devuelve un error.

los assertEqual() y assertNotEqual() son funciones que se utilizan para validar la salida de nuestra calculadora. los assertEqual() La función comprueba si los dos valores proporcionados son iguales, en nuestro caso, esperamos la suma de 5 y 6 ser – estar 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 ofrecidas por unittest incluir:

  • assertTrue(a): Comprueba si la expresión proporcionada es true
  • assertGreater(a, b): Comprueba si a es mayor que b
  • assertNotIn(a, b): Comprueba si a es en b
  • assertLessEqual(a, b): Comprueba si a es menor o igual a b
  • etc …

Puede encontrar una lista de estas afirmaciones en esta hoja de trucos.

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

$ 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 del tearDown() función mediante 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. los unittest marco indica los tres escenarios mediante:

  • Un punto final (.): Indica una prueba aprobada
  • 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. Recibimos errores porque aún no hemos implementado el addition funcionalidad de nuestra calculadora:

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

Nuestra calculadora ahora está lista para sumar dos números, pero para estar seguros de que funcionará como se espera, eliminemos el tearDown() funcionar de nuestras pruebas y ejecutar nuestras pruebas una vez más:

$ 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 una vez 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 realizar la primera prueba, tenemos que refactorizar nuestra función de suma de la siguiente manera:

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

Cuando ejecutamos nuestras pruebas una vez más:

$ 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 se compara None a ERROR y como no son iguales, la prueba falla. Para hacer que nuestra prueba pase tenemos que devolver el error en nuestro sum() función:

def sum(self, a, b):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    else:
        return "ERROR"

Y cuando ejecutamos nuestras pruebas:

$ python3 test_simple_calulator.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Todas nuestras pruebas pasan ahora y obtenemos 3 paradas completas 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, el ZeroDivisionError se plantea una excepción. En nuestro DivisionTestSuite, podemos confirmar si se generó la excepción:

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)

los test_divide_by_zero_exception() ejecutará el divide(10, 0) función de nuestra calculadora y confirmar que la excepción efectivamente se planteó. Podemos ejecutar el DivisionTestSuite de forma aislada, como sigue:

$ python3 -m unittest test_simple_calulator.DivisionTestSuite.test_divide_by_zero_exception
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

El conjunto de pruebas de funcionalidad de división completa se puede encontrar en la esencia vinculada 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 unittest framework e identificó las situaciones en las que se utiliza al desarrollar programas Python. los unittest framework, también conocido como PyUnit, viene con la distribución Python por defecto a diferencia de otros frameworks de prueba. De manera TDD, escribimos las pruebas para una calculadora simple, ejecutamos las pruebas y luego implementamos la funcionalidad para que las pruebas pasaran.

los unittest framework proporcionó la funcionalidad para crear y agrupar casos de prueba y verificar la salida de nuestra calculadora con la salida esperada para verificar que esté funcionando como se esperaba.

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

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad