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ícate

Testing 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
Empezar curso de Python

Lecciones de este módulo de Python

Lecciones de programación del módulo Testing del curso de Python.