Depuración y pruebas

Ingeniería del Software para Inteligencia Artificial

Recordatorio

En la anterior sesión vimos los conceptos básicos de Python.

  • Almacenamiento de datos en variables
  • Uso de condicionales para tomar decisiones
  • Creación de funciones para reutilizar código
  • Uso de bucles para repetir tareas o iterar colecciones

Tabla de contenidos

Detección de errores

¿Cómo sabemos si nuestro código funciona correctamente?

def una_funcion_muy_complicada():
    # Código extremadamente complicado
    print("Hola")
    # Más código extremadamente complicado
    print("Test parte 2")
    # Todavía más código extremadamente complicado
    print("asdfasdfalsdhfl")
    # Y más código extremadamente complicado
    return 42

una_funcion_muy_complicada()

No se debe hacer esto

Malas prácticas

Causan
Errores dificiles de encontrar y corregir

Cuando
Se usan técnicas que no son adecuadas para el propósito que se pretende

Malas prácticas

Nosotros, como ingenieros del software, debemos ser capaces de identificar estas malas prácticas y evitarlas.

Malas prácticas

El método print

  • … no detecta errores automáticamente
  • … no proporciona información estructurada
  • … dificulta encontrar errores en códigos complejos
  • … no se puede desactivar fácilmente

Gestión de informes para controlar el flujo del programa

Sistemas de logging

Python tiene un sistema de mensajes por nivel de importancia.

import logging
logging.debug("Este es un mensaje de depuración.")
logging.info("Este es un mensaje informativo.")
logging.warning("Este es una advertencia.")
logging.error("Este es un mensaje de error.")
logging.critical("Este es un error crítico.")
print("Esto es un mensaje normal.")
WARNING:root:Este es una advertencia.
ERROR:root:Este es un mensaje de error.
CRITICAL:root:Este es un error crítico.
Esto es un mensaje normal.

Sistemas de logging

Python tiene un sistema de mensajes por nivel de importancia.

import logging
logging.debug("Este es un mensaje de depuración.")
logging.info("Este es un mensaje informativo.")
logging.warning("Este es una advertencia.")
logging.error("Este es un mensaje de error.")
logging.critical("Este es un error crítico.")
print("Esto es un mensaje normal.")

¿Dónde están los mensajes debug e info?

No aparecen porque el nivel predeterminado de logging es WARNING.

Nivel Valor Descripción
DEBUG 10 Mensajes para depuración interna.
INFO 20 Información general sobre el programa.
WARNING 30 Advertencias que no detienen el programa.
ERROR 40 Errores que pueden interrumpir la ejecución.
CRITICAL 50 Errores graves que requieren atención inmediata.

¿Cómo mostrar los mensajes de nivel DEBUG e INFO?

Cada uno de los niveles muestra los mensajes de su nivel y los de los niveles superiores.

Podemos cambiar el nivel de logging para mostrar mensajes de nivel DEBUG e INFO.

logging.basicConfig(level=logging.DEBUG)

Si ejecutamos el anterior código…

DEBUG:root:Este es un mensaje de depuración.
INFO:root:Este es un mensaje informativo.
WARNING:root:Este es una advertencia.
ERROR:root:Este es un mensaje de error.
CRITICAL:root:Este es un error crítico.
Esto es un mensaje normal.

De forma habitual, los logs se escriben en un archivo.

Para ello, hay que especificar el parámetro filename en la función basicConfig.

logging.basicConfig(filename='app.log', level=logging.INFO)

Consejo

Si se especifica un archivo, los mensajes se escribirán en él y no se mostrarán por pantalla.

Dentro de un proyecto, es recomendable usar un logger propio que nos permitirá ver de dónde provienen los mensajes.

logger = logging.getLogger("mi_inventario")
logger.info("Se ha añadido un nuevo producto.")
INFO:mi_inventario:Se ha añadido un nuevo producto.

Consejo

Es habitual especificar el nombre del logger con el nombre del modulo usando __name__.

import logging
import mylib
logger = logging.getLogger(__name__)

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logger.info('Started')
    mylib.do_something()
    logger.info('Finished')

if __name__ == '__main__':
    main()
# mylib.py
import logging
logger = logging.getLogger(__name__)

def do_something():
    logger.info('Doing something')
    # ...
    if error:
        logger.error('A mistake happened')
    # ...

Ejercicio - Ordena el código

  • import logging
  • def create_logger(name: str) -> logging.Logger:
  •  return logging.getLogger(name)
  • def solicitar_acceso(logger: logging.Logger):
  •  peticion = peticion_de_acceso()
  •  if peticion.error:
  •   logger.error(‘Se ha denegado el acceso’)
  •  else:
  •   logger.info(‘Se ha concedido el acceso’)
  • def main():
  •  logger = create_logger(__name__)
  •  solicitar_acceso(logger)
  • Gestión de errores

    Python proporciona un mecanismo para gestionar errores de forma estructurada.

    i = 5
    x = [1, 2, 3]
    try:
        print(x[i])
    except IndexError:
        print("Índice fuera de rango")
    Índice fuera de rango

    Si añadimos un bloque else, se ejecutará si no se produce ningún error.

    i = 2
    x = [1, 2, 3]
    try:
        print(x[i])
    except IndexError:
        print("Índice fuera de rango")
    else:
        print("Todo ha ido bien")
    3
    Todo ha ido bien

    Si añadimos un bloque finally, se ejecutará siempre, haya error o no.

    i = 5
    x = [1, 2, 3]
    try:
        print(x[i])
    except IndexError:
        print("Índice fuera de rango")
    finally:
        print("Fin del programa")
    Índice fuera de rango
    Fin del programa

    Si no se especifica el tipo de error, se capturarán todos los errores.

    try:
        print(x / 0)
    except:
        print("Ha ocurrido un error")
    Ha ocurrido un error

    Consejo

    No es recomendable capturar todos los errores, ya que puede ocultar errores graves.

    Para capturar el error y mostrarlo, se puede usar la variable as.

    try:
        print(x[i])
    except IndexError as e:
        print(f"Índice fuera de rango: {e}")
    Índice fuera de rango: list index out of range

    Para capturar el error y mostrarlo, se puede usar la variable as.

    try:
        print(x[i])
    except IndexError as e:
        print(f"Índice fuera de rango: {e}")
    except ZeroDivisionError as e:
        print(f"División por cero: {e}")

    Consejo

    Podemos acumular varios bloques except para gestionar diferentes tipos de errores que puedan surgir dentro del bloque try.

    Podemos lanzar un error cuando detectamos una situación inesperada.

    Utilizamos la instrucción raise seguida del tipo de error que queremos lanzar.

    def dividir(a, b):
        if b == 0:
            raise ZeroDivisionError("No se puede dividir por cero")
        return a / b
    
    try:
        dividir(5, 0)
    except ZeroDivisionError as e:
        print(f"Error: {e}")
    Error: No se puede dividir por cero

    Utilizamos la instrucción raise seguida del tipo de error que queremos lanzar.

    def dividir(a, b):
        if b == 0:
            raise ZeroDivisionError("No se puede dividir por cero")
        return a / b
    
    try:
        dividir(5, 0)
    except ZeroDivisionError as e:
        print(f"Error: {e}")

    Consejo

    Podemos crear nuestros propios tipos de error en Python, pero necesitaremos saber más sobre clases y herencia.

    Ejercicio - Completa el código

    def es_numero_valido(numero: int) -> bool: if not (numero >= 0 and numero <= 10): raise ValueError(“El número no es válido”) def poner_nota(): num = input(“Introduce una nota para el estudiante:”) try: es_numero_valido(num) except ValueError as e: print(f”Error: {e}“) else: print(”Número válido”) poner_nota() # Válido poner_nota() # No válido Introduce una nota para el estudiante: 5 Número válido Introduce una nota para el estudiante: -1 Error: El número no es válido

    Depuración

    ¿Qué es un depurador?

    Un depurador es una herramienta de programación que nos permite inspeccionar el código y controlar su ejecución.

    Encontrar errores

    Entender nuestro programa

    El depurador de VSCode

    Vamos a utilizar el depurador de Visual Studio Code para depurar nuestro código.

    Consideraciones

    Existen otros depuradores y herramientas, seleccionamos ésta porque se utiliza en entornos de trabajo reales.

    El depurador de VSCode

    Vamos a utilizar el depurador de Visual Studio Code para depurar nuestro código.

    • Abrimos el archivo que queremos depurar.
    • Pulsamos F5 o hacemos clic en el botón de Run and Debug.
    • Estipulamos una configuración de depuración.
    • Ejecutamos e inspeccionamos el código.

    Configuración de depuración

    Tenemos dos opciones:

    • Seleccionar una configuración predefinida.
    • Crear una archivo de configuración.

    Consejo

    Recomendamos crear un archivo de configuración para poder ejecutar el depurador a distintos niveles.

    Configuración de depuración

    Configuración de depuración

    Configuración de depuración

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Python: Current File",
                "type": "python",
                "request": "launch",
                "program": "${file}",
                "console": "integratedTerminal"
            },
        ]
    }

    Configuración de depuración

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Ejecuta test.py",
                "type": "python",
                "request": "launch",
                "program": "test.py",
                "console": "integratedTerminal"
            },
        ]
    }

    Configuración de depuración

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Ejecuta test.py",
                "type": "python",
                "request": "launch",
                "program": "test.py",
                "console": "integratedTerminal"
            },
            // ...
        ]
    }

    Consejo

    Es recomendable que indaguéis más sobre las posibles configuraciones https://code.visualstudio.com/docs/editor/debugging#_launch-configurations

    F5

    Hemos ejecutado el depurador ¿Y ahora qué?

    Esto no hace nada

    Python no me quiere

    Me voy a casa

    😭

    👎

    Breakpoints

    Breakpoints

    Nos permite detener la ejecución y analizar el estado del programa.

    Sin ellos, el depurador no se detendrá y no podremos inspeccionar el código.

    Podemos añadir un breakpoint haciendo clic en la barra lateral izquierda del editor.

    Si ejecutamos el depurador, el programa se detendrá en el breakpoint.

    Para movernos por el programa, podemos usar los botones de la barra de herramientas.

    • Continuar: Continúa la ejecución hasta el siguiente breakpoint.
    • Paso siguiente: Ejecuta la siguiente línea de código.
    • Paso en: Entra en la función del puntero.
    • Paso salir: Sale de la función actual.

    Para movernos por el programa, podemos usar los botones de la barra de herramientas.

    • Reiniciar: Reinicia la ejecución del programa.
    • Detener: Detiene la ejecución del programa.

    Consideraciones

    Podemos usar los atajos de teclado para movernos por el programa.

    Inspección de variables

    Podemos inspeccionar las variables en cualquier momento.

    Importante

    Solo podemos “ver” las variables que estén en el ámbito actual.

    La pestaña Watch nos permite inspeccionar fragmentos cortos de código.

    Inspección del flujo

    La pestaña Call Stack nos muestra el flujo de ejecución del programa.

    Ocurre un dilema cuando inspeccionamos bucles.

    Con un breakpoint normal, el programa se detiene en cada iteración.

    Breakpoints condicionales

    Podemos añadir condiciones a los breakpoints para que se detengan solo cuando se cumplan.

    • Clic derecho en un breakpoint y pinchar Edit Breakpoint.
    • Seleccionamos el tipo de condicion
    • Estipulamos un valor de parada

    Existen cuatro tipos de condiciones de parada:

    • Expression: Se detiene si la condición es verdadera.
    • Hit Count: Se detiene si el contador es igual al valor.
    • Log Message: Muestra un mensaje cada vez se alcanza.
    • Wait for Breakpoint: Se activa solo tras alcanzar otro breakpoint.

    Ejercicio - Contesta las preguntas

    Un depurador detecta errores automáticamente. Un breakpoint detiene la ejecución del programa. Podemos acceder a cualquier variable de nuestro programa siempre Un breakpoint dentro de una condición detiene la ejecución solo si se cumple la condición Para iterar un bucle en un breakpoint, debemos añadir un breakpoint en cada iteración

    Pruebas de código

    No es escalable probar nuestro código manualmente.

    No es escalable probar nuestro código manualmente.

    https://informationisbeautiful.net/visualizations/million-lines-of-code/

    No es escalable probar nuestro código manualmente.

    https://informationisbeautiful.net/visualizations/million-lines-of-code/

    No es escalable probar nuestro código manualmente.

    https://informationisbeautiful.net/visualizations/million-lines-of-code/

    No es escalable probar nuestro código manualmente.

    https://informationisbeautiful.net/visualizations/million-lines-of-code/

    Pruebas automatizadas

    Verifican que nuestro código funciona correctamente.

    • Detectan fallos antes de que lleguen a producción.
    • Facilitan la detección de errores.

    Y lo más importante… son automáticas

    En un proyecto, las pruebas se suelen guardar en un directorio llamado tests.

    Y son independientes del código principal.

    📁 Proyecto/
     ├── 📁 src/        # Código principal
     │    ├── main.py
     │    ├── utils.py
     │    └── ...
     ├── 📁 tests/      # Pruebas automatizadas 🧪
     │    ├── test_main.py
     │    ├── test_utils.py
     │    └── ...
     └── README.md

    Creación y ejecución de pruebas

    Para crear pruebas, necesitamos un framework de testing.

    Python tiene el suyo propio, llamado unittest.

    Un test en Python en un clase que hereda de unittest.TestCase

    … y cuyos métodos, que empiezan por test_, son los tests.

    import unittest
    
    class SimpleTest(unittest.TestCase):
        def test_true(self):
            self.assertTrue(True)

    … y cuyos métodos, que empiezan por test_, son los tests.

    import unittest
    
    class SimpleTest(unittest.TestCase):
        def test_true(self):
            self.assertTrue(True)

    Hay funciones predefinidas para validar variables y resultados.

    Consideraciones

    Podéis ver todos los métodos de aserción en la documentación oficial https://docs.python.org/3/library/unittest.html#assert-methods

    Para ejecutar los tests debemos llamar a la función unittest.main().

    import unittest
    
    class SimpleTest(unittest.TestCase):
        def test_true(self):
            self.assertTrue(True)

    Para ejecutar los tests debemos llamar a la función unittest.main().

    import unittest
    
    class SimpleTest(unittest.TestCase):
        def test_true(self):
            self.assertTrue(True)
    
    if __name__ == '__main__':
        unittest.main()

    import unittest
    
    class SimpleTest(unittest.TestCase):
        def test_true(self):
            self.assertTrue(True)
    
    if __name__ == '__main__':
        unittest.main()
    test_true (__main__.SimpleTest.test_true) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s
    
    OK
    

    Podemos ejecutar los tests desde la terminal

    …o podemos utilizar VSCode para ejecutarlos.

    Consideraciones

    Solo voy a explicar cómo ejecutar los tests desde VSCode. Podéis revisar la interfaz por terminal en la documentación oficial. https://docs.python.org/3/library/unittest.html#test-discovery

    Ejercicio - Completa el código

    import unittest class StringTests(unittest.TestCase): def test_uppercase(self): “““Comprueba que ‘hola’.upper() convierte a ‘HOLA’”““ self.assertEqual(‘hola’.upper(), ‘HOLA’) def test_split(self): ”““Comprueba que ‘Hola Mundo’.split() separa correctamente”““ self.assertEqual(‘Hola Mundo’.split(), [‘Hola’, ‘Mundo’]) def test_is_string(self): ”““Comprueba que ‘Hola’ es una instancia de str”““ self.assertIsInstance(‘Hola’, str) if __name__ ==”__main__“: unittest.main() test_split (__main__.StringTests) … ok test_uppercase (__main__.StringTests) … ok test_is_string (__main__.StringTests) … ok ———————————————————————- Ran 3 test in 0.000s OK

    Tutorial

    import unittest
    
    class MathTests(unittest.TestCase):
        def test_sum(self):
            """Comprueba que la suma de 2 + 2 es 4"""
            self.assertEqual(2 + 2, 4)
    
        def test_multiplication(self):
            """Comprueba que la multiplicación de 3 * 3 es 9"""
            self.assertEqual(3 * 3, 9)
    
    if __name__ == "__main__":
        unittest.main()
    import unittest
    
    class StringTests(unittest.TestCase):
        def test_upper(self):
            """Comprueba que 'hola' en mayúsculas es 'HOLA'"""
            self.assertEqual("hola".upper(), "HOLA")
    
        def test_startswith(self):
            """Comprueba que 'Python' empieza con 'Py'"""
            self.assertTrue("Python".startswith("Py"))
    
    if __name__ == "__main__":
        unittest.main()

    Consideraciones

    Más adelante veremos en profundidad este tema. Cómo asegurar que los tests son independientes, que se prueban todas las posibles combinaciones, etc.

    Resumen

    Se ha estudiado cómo mantener un código limpio y ordenado a la vez que se generan informes de errores y advertencias mediante el sistema de logging de Python.

    También hemos visto la importancia de manejar y generar nuestros propios errores de manera estructurada.

    Por último, hemos aprendido a depurar nuestro código con el depurador de Visual Studio Code y a generar pruebas automatizadas con el framework unittest.

    Pasos a seguir

    • Considerar el uso de logging en tu código.
    • Practicar con el depurador de Visual Studio Code.
    • Crear pruebas automatizadas para tu código.
    • Investigar más sobre el sistema de logging de Python.

    Recursos

    Fuentes de documentación

    Recursos

    Expansión de la temática

    Ejercicios

    Los ejercicios para practicar estos conceptos están disponibles en la web de la asignatura.