Saltar al contenido
Portada » Lenguajes » 11. Testing y Depuración

11. Testing y Depuración

Testing y Depuración. En este capítulo exploraremos dos aspectos fundamentales del desarrollo de software: testing y depuración. Estos procesos son esenciales para identificar y corregir errores y así garantizar que tu código funcione correctamente. Un programa puede estar bien diseñado, pero sin pruebas y depuración adecuadas es difícil asegurar su fiabilidad, rendimiento y calidad.

¿Qué aprenderás en este capítulo?

  1. Depuración de Código:
    • Técnicas básicas como el uso de print para rastrear valores de variables en un punto concreto del código.
    • Uso de herramientas más avanzadas como el depurador pdb, que permite ejecutar tu programa paso a paso para localizar errores.
  2. Introducción a Testing:
    • Pruebas unitarias: Asegúrate de que las partes individuales de tu código funcionen como se espera utilizando herramientas como unittest.
    • Pruebas avanzadas con pytest: Conocerás esta herramienta moderna y flexible que simplifica la escritura y ejecución de pruebas.
  3. Buenas Prácticas:
    • Cómo escribir pruebas que cubran la mayor cantidad posible de escenarios.
    • La importancia de probar tanto casos exitosos como escenarios de fallo.
    • Organización del código de pruebas para mantenerlo limpio y eficiente.

¡¡Vamos!!

Depuración de Código con print()

La depuración es el proceso de identificar y corregir errores en el código. Uno de los métodos más simples y comunes es el uso de declaraciones print() para inspeccionar el estado de las variables en un punto determinado del código.

Uso de print():
def suma(a, b):
print(f"Sumando {a} y {b}") # Imprimir valores antes de la operación
return a + b

resultado = suma(5, 10)
print("Resultado:", resultado)

Aunque útil y sencillo, este método puede volverse engorroso en programas más grandes y complejos. Por ello, Python incluye el módulo pdb, que proporciona un depurador interactivo más robusto.

Depuración de Código con pdb:

El depurador pdb (Python Debugger) es una herramienta interactiva incorporada en Python que permite analizar y depurar programas paso a paso. Es especialmente útil para identificar errores y entender cómo fluye el código durante la ejecución. A diferencia de print que simplemente permite comprobar valores, pdb ofrece un control mucho más granular sobre el flujo del programa lo que lo hace ideal para una depuración mucho más avanzada.


¿Qué puedes hacer con pdb?

  1. Ejecutar código paso a paso: Examinar cómo se ejecutan las líneas de tu programa.
  2. Inspeccionar variables: Ver los valores de las variables en cualquier punto de la ejecución.
  3. Establecer puntos de interrupción: Detener la ejecución en ubicaciones específicas del código para analizar el estado actual.
  4. Modificar el flujo del programa: Saltar a diferentes partes del código o continuar la ejecución desde un punto específico.
  5. Examinar errores: Analizar un error después de que el programa haya fallado.

Cómo usar pdb

1. Iniciar el depurador

Hay varias formas de activar pdb en tu programa:

Desde el terminal:
Puedes ejecutar tu script directamente con el depurador utilizando el siguiente comando:

python -m pdb tu_archivo.py

Esto ejecutará tu programa en modo depuración, permitiéndote interactuar con él línea por línea.

Insertar un punto de interrupción en el código:
Utiliza la función pdb.set_trace() para pausar la ejecución en un punto específico:

import pdb

def calcular_area(base, altura):
area = base * altura
pdb.set_trace() # Aquí se pausará la ejecución
return area

resultado = calcular_area(5, 10)
print(f"El área es {resultado}")

Cuando la ejecución llega a pdb.set_trace() se abre una consola interactiva de pdb.


2. Comandos básicos de pdb

Control del flujo del programa:

Una vez dentro de la consola de depuración puedes utilizar los siguientes comandos:

  • n (next): Ejecuta la siguiente línea de código sin entrar en funciones.
  • s (step): Avanza a la siguiente línea y entra en funciones llamadas, si es necesario.
  • c (continue): Reanuda la ejecución normal del programa hasta el próximo punto de interrupción.
  • q (quit): Termina la sesión de depuración y sale del programa.

Inspección de variables y estado:

  • p nombre_variable: Muestra el valor de la variable especificada.
  • pp nombre_variable: Muestra el valor de una variable de manera más legible.
  • locals(): Lista todas las variables locales disponibles en el contexto actual.
  • globals(): Muestra las variables globales del programa.

Puntos de interrupción:

  • b línea: Establece un punto de interrupción en una línea específica.
  • b archivo:línea: Establece un punto de interrupción en otro archivo y línea.
  • l: Muestra el código alrededor de la línea actual para contexto.

Exploración del stack (llamadas):

  • w (where): Muestra la pila de llamadas actual para entender cómo se llegó al punto actual.
  • u (up): Mueve hacia arriba en la pila de llamadas.
  • d (down): Mueve hacia abajo en la pila de llamadas.

3. Ejemplo Práctico de Uso

Imagina que estás depurando una función que calcula el factorial de un número, ¿Cómo podríamos depurarlo?

import pdb

def factorial(n):
if n < 0:
raise ValueError("El número debe ser positivo.")
elif n == 0 or n == 1:
return 1
else:
pdb.set_trace() # Punto de interrupción
return n * factorial(n - 1)

resultado = factorial(5)
print(f"El factorial es {resultado}")

Cuando se ejecuta, la ejecución se detendrá en pdb.set_trace() al calcular el factorial. Desde allí, puedes ir avanzando con el comando n a lo largo del programa e inspeccionar el valor de las variables con locals(). Si continuas haciendo lo mismo en cada llamada recursiva verás como cambia el valor de n

$python -m pdb factorial.py 
> /python/factorial.py(1)<module>()
-> import pdb
(Pdb) n
> /python/factorial.py(3)<module>()
-> def factorial(n):
(Pdb) n
> /python/factorial.py(12)<module>()
-> resultado = factorial(5)
(Pdb) n
> /python/factorial.py(10)factorial()
-> return n * factorial(n - 1)
(Pdb) locals()
{'n': 5}
(Pdb) n
> /python/factorial.py(10)factorial()
-> return n * factorial(n - 1)
(Pdb) locals()
{'n': 4}
(Pdb) n
> /python/factorial.py(10)factorial()
-> return n * factorial(n - 1)
(Pdb) locals()
{'n': 3}
(Pdb) n
> /python/factorial.py(10)factorial()
-> return n * factorial(n - 1)
(Pdb) locals()
{'n': 2}
(Pdb) n
--Return--
> /python/factorial.py(10)factorial()->2
-> return n * factorial(n - 1)
(Pdb) locals()
{'n': 2, '__return__': 2}
(Pdb) 



4. Depuración Post-Mortem

A veces, es útil depurar un programa después de que haya fallado. Python ofrece esto con pdb, vamos a crear un programa para forzar un error al hacer una división por 0:

#!/bin/python3

a = 5
b = 0

resultado = a / b

print (resultado)



$ python -m pdb div_cero.py
> /python/div_cero.py(3)<module>()
-> a = 5
(Pdb) c
Traceback (most recent call last):
  File "/usr/lib/python3.12/pdb.py", line 1944, in main
    pdb._run(target)
  File "/usr/lib/python3.12/pdb.py", line 1738, in _run
    self.run(target.code)
  File "/usr/lib/python3.12/bdb.py", line 600, in run
    exec(cmd, globals, locals)
  File "<string>", line 1, in <module>
  File "/python/div_cero.py", line 6, in <module>
    resultado = a / b
                ~~^~~
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /python/div_cero.py(6)<module>()
-> resultado = a / b
(Pdb) --KeyboardInterrupt--

Si ocurre un error, pdb se activará automáticamente en el punto donde ocurrió el fallo, y podrás inspeccionar el estado del programa.

5. Consejos para Usar pdb

  • Usa puntos de interrupción estratégicamente en áreas donde sospechas que podría haber problemas.
  • Combina pdb con pruebas automatizadas para depurar casos específicos.
  • Familiarízate con los comandos básicos; esto hará que la depuración sea más rápida y eficiente.
  • Considera herramientas gráficas basadas en pdb, como pdb++ o plugins de los IDEs de programación para una experiencia más visual.

Testing. (unittest y pytest)

El testing es una parte fundamental del desarrollo de software que asegura que nuestro código funciona según lo esperado. Python ofrece herramientas para realizar pruebas, siendo unittest y pytest dos de las más populares. Aunque ambas cumplen con el propósito de probar el código, tienen características particulares que las hacen adecuadas para diferentes necesidades.

Unittest. El Módulo Estándar de Python para Pruebas

unittest es el framework estándar de Python para escribir y ejecutar pruebas. Está inspirado en el modelo xUnit, que es utilizado en muchos lenguajes de programación.

Características Clave

  1. Soporte para Pruebas Unitarias y Funcionales: Ideal para probar funciones o clases individuales.
  2. Integración con Python: Incluido en la biblioteca estándar, lo que significa que no necesitas instalar paquetes adicionales.
  3. Estructura Formal: Requiere que organices tus pruebas dentro de clases y métodos.

Estructura de una Prueba en unittest

Para usar unittest, debes crear clases que hereden de unittest.TestCase. Cada método que comience con test_ será reconocido como una prueba.

Ejemplo Básico con unittest

import unittest

# Función a probar
def sumar(a, b):
return a + b

# Clase de prueba
class TestFunciones(unittest.TestCase):

def test_sumar_numeros(self):
self.assertEqual(sumar(2, 3), 5) # Verifica que la suma sea correcta

def test_sumar_cadenas(self):
self.assertEqual(sumar("Hola ", "Mundo"), "Hola Mundo") # Verifica la concatenación de cadenas

# Ejecutar las pruebas
if __name__ == '__main__':
unittest.main()

Explicación del Código

  • unittest.TestCase: Todas las clases de prueba deben heredar de esta clase base.
  • assertEqual: Compara si el resultado de sumar(2, 3) es igual a 5.
  • unittest.main(): Ejecuta todas las pruebas en el archivo.

Métodos Comunes de unittest

  1. assertEqual(a, b): Verifica que a sea igual a b.
  2. assertNotEqual(a, b): Verifica que a no sea igual a b.
  3. assertTrue(expr): Verifica que la expresión expr sea verdadera.
  4. assertFalse(expr): Verifica que la expresión expr sea falsa.
  5. assertRaises(Exception, callable): Verifica que se levante una excepción.

Configuración Previa y Limpieza con setUp y tearDown

Puedes inicializar recursos antes de cada prueba con setUp y liberar recursos con tearDown.

pythonCopiar códigoclass TestConRecursos(unittest.TestCase):
    def setUp(self):
        self.recurso = "Recurso inicializado"
    
    def tearDown(self):
        self.recurso = None
    
    def test_recurso(self):
        self.assertIsNotNone(self.recurso)

pytest. Framework para Pruebas

pytest es un framework más reciente que simplifica la escritura de pruebas, haciendo el proceso más flexible y menos formal que unittest.

Características Clave de pytest

  1. Menos Verbosidad: No necesitas usar clases, aunque son compatibles.
  2. Ampliable: Permite usar plugins y personalizar el comportamiento.
  3. Detección Automática de Pruebas: Detecta funciones que comienzan con test_.
  4. Mejor Salida de Errores: Ofrece mensajes claros cuando una prueba falla.

Estructura de una Prueba en pytest

En pytest, puedes escribir pruebas simplemente definiendo funciones con el prefijo test_.

Ejemplo Básico con pytest

# Función a probar
def multiplicar(a, b):
return a * b

# Pruebas
def test_multiplicar_numeros():
assert multiplicar(3, 4) == 12

def test_multiplicar_cadenas():
assert multiplicar("Hola", 3) == "HolaHolaHola"

Para ejecutar estas pruebas, simplemente usa:

pytest nombre_del_archivo.py

Ventajas de pytest

  1. Sin Clases Necesarias: Puedes escribir funciones en lugar de clases.
  2. Asserts Sencillos: Usa el operador assert nativo de Python.
  3. Configuraciones Avanzadas: Soporte para fixtures y parametrización.

Comparación de unittest y pytest

Característicaunittestpytest
VerbosidadRequiere clases y métodos formalesFunciones simples con assert
Detección AutomáticaLimitadaExcelente
PluginsNo soporta pluginsAmplia biblioteca de plugins
CompatibilidadIncluido en PythonRequiere instalación
Curva de AprendizajeLigeramente más pronunciadaMás intuitivo y rápido

Casos Prácticos Avanzados

Uso de Fixtures en pytest

Los fixtures son funciones especiales en pytest que se utilizan para inicializar datos antes de ejecutar las pruebas.

import pytest

@pytest.fixture
def datos_ejemplo():
return {"nombre": "Ana", "edad": 30}

def test_datos(datos_ejemplo):
assert datos_ejemplo["edad"] == 30

Pruebas Parametrizadas con pytest

Permite ejecutar una misma prueba con diferentes valores de entrada.

import pytest

@pytest.mark.parametrize("a,b,esperado", [
(2, 3, 5),
(-1, 1, 0),
(10, 5, 15)
])
def test_suma_parametrizada(a, b, esperado):
assert a + b == esperado

Conclusión

Tanto unittest como pytest son herramientas para realizar pruebas en Python. Mientras que unittest es formal y ampliamente utilizado debido a su inclusión en la biblioteca estándar, pytest es más flexible y fácil de usar, siendo la elección preferida para proyectos modernos.

Dependiendo de tus necesidades y del tamaño de tu proyecto, puedes optar por uno u otro, o incluso combinarlos si es necesario.


Resumen

El testing y la depuración son elementos esenciales del desarrollo de software que garantizan la calidad y confiabilidad del código. Utilizar herramientas como print() y pdb para depurar y bibliotecas como unittest y pytest para realizar pruebas unitarias permite a los desarrolladores identificar y corregir errores. Aplicar buenas prácticas en la escritura de pruebas no solo mejora la calidad del software, sino que también facilita el mantenimiento y la evolución del código a lo largo del tiempo.

Pasemos a la parte práctica!!!!

Ejercicio 1: Depuración con pdb

Tienes un programa que calcula la suma de los cuadrados de los números en una lista. Sin embargo, parece que no está funcionando correctamente. Usa pdb para depurar el programa y encontrar el error.
import pdb

def suma_cuadrados(lista):
    resultado = 0
    for num in lista:
        pdb.set_trace()  # Punto de interrupción
        resultado += num ** 2
    return resultado

# Lista de ejemplo
numeros = [1, 2, '3', 4]

# Llamada a la función
print(suma_cuadrados(numeros))

Explicación

  • Punto de Interrupción: La sentencia pdb.set_trace() detiene la ejecución en ese punto, permitiéndote inspeccionar variables y ejecutar comandos interactivos.
  • Error Detectado: El error ocurre al intentar elevar '3' (una cadena) al cuadrado.
  • Solución: Asegurarte de que todos los elementos de la lista sean números.

Versión Corregida

def suma_cuadrados(lista):
resultado = 0
for num in lista:
if isinstance(num, (int, float)):
resultado += num ** 2
else:
print(f"Ignorado elemento no numérico: {num}")
return resultado

numeros = [1, 2, '3', 4]
print(suma_cuadrados(numeros)) # Salida: 21
Ejercicio 2: Prueba Unitaria con unittest

Escribe una prueba unitaria para verificar el funcionamiento correcto de una función que calcula el factorial de un número. La función debe devolver 1 si el número es 0 o 1, y multiplicar los números consecutivos en caso contrario.
import unittest

def factorial(n):
    if n == 0 or n == 1:
        return 1
    elif n > 1:
        resultado = 1
        for i in range(2, n + 1):
            resultado *= i
        return resultado
    else:
        raise ValueError("El número debe ser mayor o igual a 0.")

class TestFactorial(unittest.TestCase):
    def test_factorial_cero(self):
        self.assertEqual(factorial(0), 1)

    def test_factorial_uno(self):
        self.assertEqual(factorial(1), 1)

    def test_factorial_positivo(self):
        self.assertEqual(factorial(5), 120)

    def test_factorial_error(self):
        with self.assertRaises(ValueError):
            factorial(-3)

if __name__ == '__main__':
    unittest.main()

Explicación

  • Función Probada: factorial.
  • Casos Cubiertos:
    • Factorial de 0 y 1.
    • Factorial de un número positivo.
    • Manejo de valores negativos que generan excepciones.
  • Método assertRaises: Verifica que se lance una excepción específica.
Ejercicio 3: Pruebas Parametrizadas con pytest

Crea una función que determine si un número es primo. Escribe pruebas parametrizadas usando pytest para verificar su funcionalidad con múltiples valores.
import pytest

def es_primo(n):
    if n <= 1:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

@pytest.mark.parametrize("numero, esperado", [
    (1, False),  # 1 no es primo
    (2, True),   # 2 es primo
    (9, False),  # 9 no es primo
    (17, True),  # 17 es primo
    (25, False), # 25 no es primo
])
def test_es_primo(numero, esperado):
    assert es_primo(numero) == esperado

Explicación

  • Función Probada: es_primo, que verifica si un número es primo.
  • Parametrización: Permite probar varios casos con diferentes entradas y resultados esperados.
  • Ejecución: Usa el comando pytest nombre_del_archivo.py para ejecutar las pruebas.

Bibliografía de Python de interés.

Logo Python,. esting y Depuración

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Disponible para Amazon Prime