Python Unittest frente a Pytest

Salman Mehmood 10 octubre 2023
Python Unittest frente a Pytest

El objetivo principal de este artículo es discutir dos de los marcos más utilizados para pruebas unitarias en Python, unittest y pytest, sus ventajas y desventajas, y cuándo preferir cuál sobre el otro.

Python prueba unitaria frente a Pytest

Al escribir cualquier software, debemos mantener el proceso de verificación de errores durante todo el proceso de desarrollo. Garantiza que una vez que el software alcance la etapa de lanzamiento, se encuentre una cantidad mínima de errores durante su uso.

Python también tiene una variedad de marcos de prueba que permiten probar el código escrito dándole entradas variadas para verificar su comportamiento.

En caso de que se encuentre algún error, puede corregirse durante las etapas de desarrollo en lugar de “revisiones” después del lanzamiento inicial de la aplicación.

Código de ejemplo:

class Calculate:
    def CheckPrime(self, a):
        for i in range(a):
            if a % i:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.fact(a - 1)

El código que se muestra arriba contiene dos funciones llamadas CheckPrime y CalcFact, que, como se desprende de sus nombres, verifican números primos y calculan factoriales.

Para garantizar que los métodos de Calcular funcionen sin problemas, es esencial verificar los errores que pueden surgir al dar resultados variados.

Entonces, ¿cómo podemos hacer eso? Para asegurarnos de que nuestro código esté libre de errores, podemos usar diferentes marcos de prueba para escribir casos de prueba y probar nuestro código encima de ellos para verificar la integridad de nuestro código.

Aunque existen muchos marcos de prueba, dos de los más utilizados son unittest y pytest. Vamos a explorarlos uno por uno a continuación.

Prueba unitaria por el marco unittest

unittest es un marco de pruebas unitarias incluido en la biblioteca estándar de Python. Este marco se inspiró en JUnit, un marco de Java para pruebas unitarias.

Antes de discutir el funcionamiento de unittest, es esencial conocer los términos comúnmente utilizados en unittest (también utilizados en otros marcos relacionados).

  • Caso de prueba – Unidad de prueba más pequeña – Por lo general, consta de un solo
  • Test Suite – Casos de prueba agrupados – Suele ejecutarse uno tras otro
  • Test Runner: coordina y maneja la ejecución de casos y suites de prueba

Use el marco unittest para escribir casos de prueba

Dado que la biblioteca estándar de Python ya contiene unittest, no es necesario descargar ningún módulo externo para comenzar a escribir pruebas unitarias usando unittest.

Podemos comenzar después de importar el módulo unittest. Ahora, concentrémonos en el código que hemos visto antes.

Código de ejemplo:

class Calculate:
    def CheckPrime(self, a):
        for i in range(a):
            if a % i:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.fact(a - 1)

Para escribir casos de prueba utilizando unittest, tenemos que seguir una sintaxis específica, a saber, que la clase de prueba es un hijo de unittest.TestCase, y sus métodos deben comenzar con test_.

Considere el siguiente código:

import unittest


class Calculate:
    def CheckPrime(self, a):
        for i in range(2, a):
            if a % i == 0:
                return False
        return True

    def CalcFact(self, a):
        if a == 1:
            return a
        else:
            return a * self.CalcFact(a - 1)


class TestCalc(unittest.TestCase):
    def test_CheckPrime(self):
        calc = Calculate()

        # Passing different outputs
        self.assertEqual(calc.CheckPrime(2), True)
        self.assertEqual(calc.CheckPrime(3), True)

        self.assertEqual(calc.CheckPrime(4), False)
        self.assertEqual(calc.CheckPrime(80), False)

    def test_CheckFact(self):
        calc = Calculate()

        # Passing different outputs
        self.assertEqual(calc.CalcFact(2), 2)
        self.assertEqual(calc.CalcFact(3), 6)

Producción :

PS D:\Unittest> python -m unittest a.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK 

A juzgar por el resultado, podemos ver que todos los casos de prueba pasaron porque todas las afirmaciones fueron exitosas.

Ahora probemos un caso en el que falla el caso de prueba.

def test_CheckFact(self):
    calc = Calculate()

    # Passing different outputs
    self.assertEqual(calc.CalcFact(2), 2)
    self.assertEqual(calc.CalcFact(3), 6)
    # Supposed to throw an error
    self.assertEqual(calc.CalcFact(0), 0)

Producción :

PS D:\Unittest> python -m unittest a.py
======================================================================
ERROR: test_CheckFact (a.TestCalc)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\Python Articles\a.py", line 34, in test_CheckFact
    self.assertEqual(calc.CalcFact(0), 0) # Supposed to throw an error
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  File "D:\Python Articles\a.py", line 15, in CalcFact
    return a * self.CalcFact(a-1)
  [The previous line is repeated 974 more times]
  File "D:\Python Articles\a.py", line 12, in CalcFact
    if (a == 1):
RecursionError: maximum recursion depth exceeded in comparison

----------------------------------------------------------------------
Ran 2 tests in 0.004s
FAILED (errors=1)

Como se desprende del código, ejecutamos el script usando python -m unittest <name_of_script.py>.

Este código funciona sin llamar a los métodos de la clase de prueba porque el módulo unittest maneja los archivos de script que se le dan en un formato particular.

Dado que nuestro script contenía TestCalc, la clase secundaria de unittest.TestCase es instanciada automáticamente por Test Runner.

Después de la creación de instancias, los métodos de prueba se encuentran dentro de la clase y se ejecutan en orden. Para que un método sea considerado un método de prueba, debe comenzar con test_.

Una vez que se encuentran los métodos de prueba, se llaman en orden; en nuestro caso, se llaman tanto test_CheckPrime como test_CalcFact. Las afirmaciones se verifican en nuestra implementación y se arroja un error en la salida en caso de un comportamiento inesperado.

De nuestro caso de prueba, que contenía un error, se puede deducir que debido a cómo está escrito el código, comenzó a ocurrir una recursividad infinita en el método CalcFact, que ahora se puede solucionar gracias al caso de prueba.

En caso de que se pregunte por qué ocurre el error, se debe a que la condición inicial no verifica los números menores que uno.

Pros y contras del marco unittest

Algunas de las ventajas de usar unittest se enumeran a continuación:

  • Incluido en la biblioteca estándar de Python
  • Promueve casos de prueba relacionados en un único conjunto de pruebas
  • Colección de prueba rápida
  • Duración precisa del tiempo de prueba

El unittest viene con las siguientes desventajas:

  • Puede ser difícil de entender
  • Sin salida de color
  • Puede ser demasiado detallado

Prueba unitaria por Pytest Framework

A diferencia de unittest, Pytest no es un módulo integrado; Tenemos que descargarlo por separado. Sin embargo, instalar Pytest es relativamente fácil; para ello podemos usar pip y ejecutar el siguiente comando.

pip install pytest

Use Pytest para escribir casos de prueba

Escribamos algunos casos de prueba usando Pytest. Sin embargo, antes de comenzar, veamos en qué se diferencia Pytest de unittest al escribir casos de prueba. Para pruebas unitarias escritas en Pytest, tenemos que:

  • Cree un directorio separado y coloque los scripts que se van a probar en el directorio recién creado.
  • Escribir pruebas en archivos que comiencen con test_ o terminen con _test.py. Un ejemplo sería test_calc.py o calc_test.py.

Considere el siguiente código escrito para casos de prueba usando Pytest.

def test_CheckPrime():
    calc = Calculate()

    # Passing different outputs
    assert calc.CheckPrime(2) == True
    assert calc.CheckPrime(3) == True

    assert calc.CheckPrime(4) == False
    assert calc.CheckPrime(80) == False


def test_CheckFact():
    calc = Calculate()

    # Passing different outputs
    assert calc.CalcFact(2) == 2
    assert calc.CalcFact(3) == 6
    # assert calc.CalcFact(0) == 0 # Supposed to throw an error

Producción :

============================================================== test session starts ==============================================================
platform win32 -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Unittest
collected 2 items

test_a.py
[100%]

=============================================================== 2 passed in 0.04s ===============================================================

Ahora, con un caso de prueba fallido:

============================================================== test session starts ==============================================================
platform win32 -- Python 3.10.7, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Unittest
collected 2 items

test_a.py .F
[100%]

=================================================================== FAILURES ====================================================================
________________________________________________________________ test_CheckFact _________________________________________________________________

    def test_CheckFact():
        calc = Calculate()

        # Passing different outputs
        assert calc.CalcFact(2) == 2
        assert calc.CalcFact(3) == 6
>       assert calc.CalcFact(0) == 0 # Supposed to throw an error

test_a.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
test_a.py:13: in CalcFact
    return a * self.CalcFact(a-1)
.
.
.
.
.
RecursionError: maximum recursion depth exceeded in comparison

test_a.py:10: RecursionError
============================================================ short test summary info ============================================================ 
FAILED test_a.py::test_CheckFact - RecursionError: maximum recursion depth exceeded in comparison
========================================================== 1 failed, 1 passed in 2.42s ========================================================== 

Los casos de prueba escritos usando Pytest son un poco más simples que unittest; en lugar de crear una clase que fuera hija de unittest.TestCase, podemos escribir nuestras funciones de prueba simplemente con test_ al comienzo del método.

Pros y contras del marco Pytest

Las siguientes son algunas ventajas de usar el marco Pytest en Python.

  • Suites de prueba compactas

  • Código repetitivo mínimo

  • Soporte de complementos

  • Presentación de salida ordenada y adecuada

    También viene con una desventaja, que se enumera a continuación.

  • A menudo incompatible con otros marcos

Salman Mehmood avatar Salman Mehmood avatar

Hello! I am Salman Bin Mehmood(Baum), a software developer and I help organizations, address complex problems. My expertise lies within back-end, data science and machine learning. I am a lifelong learner, currently working on metaverse, and enrolled in a course building an AI application with python. I love solving problems and developing bug-free software for people. I write content related to python and hot Technologies.

LinkedIn

Artículo relacionado - Python Unit Test