Python: Testing
Descubre cómo realizar testing en Python con unittest y pytest para garantizar código fiable y mantenible mediante pruebas automatizadas.
Aprende Python GRATIS y certifícateTesting en Python
El testing o pruebas de software constituye una disciplina fundamental en el desarrollo moderno que garantiza la calidad, fiabilidad y mantenibilidad del código. En Python, el ecosistema de testing es especialmente rico y accesible, proporcionando herramientas que van desde la biblioteca estándar hasta frameworks especializados que facilitan la implementación de estrategias de pruebas robustas.
Fundamentos del testing
Las pruebas automatizadas representan el proceso de verificar que el código funciona según las especificaciones definidas. Este enfoque permite detectar errores de forma temprana, facilita el mantenimiento del código y proporciona confianza al realizar cambios o refactorizaciones.
Python incluye en su biblioteca estándar el módulo unittest, que implementa el patrón xUnit utilizado en múltiples lenguajes de programación. Este framework proporciona una base sólida para estructurar y ejecutar pruebas de manera organizada.
import unittest
class TestCalculadora(unittest.TestCase):
def test_suma(self):
resultado = 2 + 3
self.assertEqual(resultado, 5)
def test_division_por_cero(self):
with self.assertRaises(ZeroDivisionError):
resultado = 10 / 0
if __name__ == '__main__':
unittest.main()
Tipos de pruebas
Las pruebas unitarias se centran en verificar el comportamiento de componentes individuales del código, típicamente funciones o métodos específicos. Estas pruebas deben ser rápidas, independientes y focalizadas en una única funcionalidad.
def calcular_area_rectangulo(base, altura):
if base <= 0 or altura <= 0:
raise ValueError("Las dimensiones deben ser positivas")
return base * altura
class TestAreaRectangulo(unittest.TestCase):
def test_area_correcta(self):
area = calcular_area_rectangulo(5, 3)
self.assertEqual(area, 15)
def test_dimensiones_negativas(self):
with self.assertRaises(ValueError):
calcular_area_rectangulo(-5, 3)
Las pruebas de integración verifican que diferentes componentes del sistema funcionen correctamente cuando se combinan. Estas pruebas son especialmente importantes cuando se trabaja con bases de datos, APIs externas o sistemas de archivos.
Pytest: el framework moderno
Pytest ha emergido como el framework de testing más popular en la comunidad Python debido a su sintaxis simple y características avanzadas. Su filosofía se basa en escribir pruebas que sean fáciles de leer y mantener.
def test_suma_basica():
assert 2 + 3 == 5
def test_lista_vacia():
lista = []
assert len(lista) == 0
assert not lista # Lista vacía es falsy
def test_string_contiene_palabra():
texto = "Python es genial"
assert "Python" in texto
assert texto.startswith("Python")
Una de las características más destacadas de pytest son las fixtures, que permiten configurar el estado necesario para las pruebas de manera reutilizable y eficiente.
import pytest
@pytest.fixture
def lista_numeros():
return [1, 2, 3, 4, 5]
@pytest.fixture
def archivo_temporal(tmp_path):
archivo = tmp_path / "test.txt"
archivo.write_text("contenido de prueba")
return archivo
def test_suma_lista(lista_numeros):
resultado = sum(lista_numeros)
assert resultado == 15
def test_lectura_archivo(archivo_temporal):
contenido = archivo_temporal.read_text()
assert contenido == "contenido de prueba"
Parametrización de pruebas
La parametrización permite ejecutar la misma prueba con diferentes conjuntos de datos, reduciendo la duplicación de código y mejorando la cobertura de casos de prueba.
@pytest.mark.parametrize("base,altura,esperado", [
(2, 3, 6),
(5, 4, 20),
(1, 1, 1),
(10, 0.5, 5.0)
])
def test_area_rectangulo_parametrizada(base, altura, esperado):
resultado = calcular_area_rectangulo(base, altura)
assert resultado == esperado
@pytest.mark.parametrize("entrada,esperado", [
("hola", "HOLA"),
("Python", "PYTHON"),
("", ""),
("123", "123")
])
def test_conversion_mayusculas(entrada, esperado):
assert entrada.upper() == esperado
Mocking y simulación
El mocking es una técnica esencial para aislar el código bajo prueba de sus dependencias externas. Python proporciona el módulo unittest.mock
que permite crear objetos simulados que imitan el comportamiento de componentes reales.
from unittest.mock import Mock, patch
import requests
def obtener_datos_usuario(user_id):
response = requests.get(f"https://api.ejemplo.com/users/{user_id}")
if response.status_code == 200:
return response.json()
return None
@patch('requests.get')
def test_obtener_datos_usuario_exitoso(mock_get):
# Configurar el mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "nombre": "Juan"}
mock_get.return_value = mock_response
# Ejecutar la función
resultado = obtener_datos_usuario(1)
# Verificar resultados
assert resultado == {"id": 1, "nombre": "Juan"}
mock_get.assert_called_once_with("https://api.ejemplo.com/users/1")
Cobertura de código
La cobertura de código mide qué porcentaje del código fuente es ejecutado durante las pruebas. Esta métrica ayuda a identificar áreas del código que no están siendo probadas y puede revelar código muerto o casos edge no considerados.
pip install coverage
coverage run -m pytest
coverage report
coverage html # Genera reporte HTML detallado
Organización de pruebas
Una estructura de pruebas bien organizada facilita el mantenimiento y la comprensión del conjunto de pruebas. La convención más común es crear un directorio tests/
que refleje la estructura del código fuente.
proyecto/
├── src/
│ ├── calculadora.py
│ ├── utils.py
│ └── models/
│ └── usuario.py
└── tests/
├── test_calculadora.py
├── test_utils.py
└── test_models/
└── test_usuario.py
Las pruebas de regresión aseguran que las funcionalidades existentes continúen trabajando correctamente después de realizar cambios en el código. Estas pruebas son especialmente valiosas en proyectos con múltiples desarrolladores o ciclos de desarrollo largos.
El desarrollo dirigido por pruebas (TDD) es una metodología donde las pruebas se escriben antes que el código de implementación. Este enfoque fuerza a pensar en el diseño de la API y los casos de uso antes de la implementación, resultando frecuentemente en código más limpio y mejor estructurado.
# Primero escribimos la prueba
def test_validar_email():
assert validar_email("usuario@ejemplo.com") == True
assert validar_email("email_invalido") == False
assert validar_email("") == False
# Luego implementamos la función
def validar_email(email):
import re
patron = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(patron, email)) if email else False
Lecciones de este módulo de Python
Lecciones de programación del módulo Testing del curso de Python.