Curso de Unit Testing en Python

Toma las primeras clases gratis
<h1>🚀 Testing Hack: Fixtures Avanzados con Context Managers</h1>

¿Por qué este hack es valioso?

Después de aprender setUp() y tearDown() en unittest, probablemente te has encontrado con código repetitivo y difícil de mantener. Este tutorial te enseñará un patrón avanzado que combina context managers con fixtures para crear pruebas más limpias, reutilizables y robustas.

El Problema

Imagina que tienes múltiples tests que necesitan:

  • Crear archivos temporales
  • Conectarse a bases de datos
  • Mockear servicios externos
  • Limpiar recursos automáticamente

Con setUp() y tearDown() tradicionales, terminas duplicando mucho código:

import unittest
import tempfile
import os

class TestTradicional(unittest.TestCase):
    def setUp(self):
        # Crear archivo temporal
        self.temp_file = tempfile.NamedTemporaryFile(delete=False)
        self.temp_file.write(b"test data")
        self.temp_file.close()
        
    def tearDown(self):
        # Limpiar
        os.unlink(self.temp_file.name)
        
    def test_leer_archivo(self):
        # Test aquí
        pass

La Solución: Context Manager Fixtures

Crea fixtures reutilizables con context managers:

from contextlib import contextmanager
import tempfile
import os
import json

@contextmanager
def temp_json_file(data):
    """
    Context manager que crea un archivo JSON temporal
    y lo limpia automáticamente.
    """
    temp_file = tempfile.NamedTemporaryFile(
        mode='w',
        delete=False,
        suffix='.json'
    )
    
    try:
        json.dump(data, temp_file)
        temp_file.flush()
        temp_file.close()
        yield temp_file.name  # Retorna el path
    finally:
        # Cleanup automático garantizado
        if os.path.exists(temp_file.name):
            os.unlink(temp_file.name)


@contextmanager
def mock_environment(**env_vars):
    """
    Context manager para mockear variables de entorno
    temporalmente.
    """
    original_env = {}
    
    # Guardar valores originales
    for key, value in env_vars.items():
        original_env[key] = os.environ.get(key)
        os.environ[key] = value
    
    try:
        yield
    finally:
        # Restaurar valores originales
        for key, original_value in original_env.items():
            if original_value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = original_value

Usando los Fixtures

Ahora tus tests son mucho más limpios:

import unittest
from tu_app import procesar_configuracion

class TestConFixtures(unittest.TestCase):
    
    def test_procesar_json(self):
        """Test usando el fixture de archivo temporal"""
        config_data = {
            "api_key": "test123",
            "timeout": 30
        }
        
        # Uso del context manager
        with temp_json_file(config_data) as json_path:
            resultado = procesar_configuracion(json_path)
            self.assertEqual(resultado['api_key'], 'test123')
            self.assertEqual(resultado['timeout'], 30)
        
        # ¡El archivo se limpia automáticamente!
    
    def test_con_variables_entorno(self):
        """Test con variables de entorno mockeadas"""
        with mock_environment(API_URL="https://test.api.com", DEBUG="True"):
            # Tu código que lee las env vars
            from tu_app import get_api_url
            self.assertEqual(get_api_url(), "https://test.api.com")
        
        # Las env vars se restauran automáticamente

Hack Avanzado: Fixtures Anidados

Puedes combinar múltiples fixtures:

@contextmanager
def temp_database():
    """Simula una conexión a base de datos"""
    db = {"users": [], "posts": []}
    
    try:
        yield db
    finally:
        db.clear()


def test_sistema_completo(self):
    """Test con múltiples fixtures anidados"""
    config = {"db_name": "test_db"}
    
    with temp_json_file(config) as config_path:
        with mock_environment(CONFIG_PATH=config_path):
            with temp_database() as db:
                # Tu test aquí con todos los recursos
                db["users"].append({"id": 1, "name": "Test"})
                self.assertEqual(len(db["users"]), 1)
    
    # TODO se limpia automáticamente en orden inverso

Fixture Reutilizable para APIs Mockeadas

Este es el más útil en proyectos reales:

from unittest.mock import patch, Mock
from contextlib import contextmanager

@contextmanager
def mock_api_response(url_pattern, response_data, status_code=200):
    """
    Mockea respuestas de APIs con requests
    """
    mock_response = Mock()
    mock_response.json.return_value = response_data
    mock_response.status_code = status_code
    mock_response.ok = status_code < 400
    
    with patch('requests.get') as mock_get:
        mock_get.return_value = mock_response
        yield mock_get


# Uso en tests
class TestAPIIntegration(unittest.TestCase):
    
    def test_obtener_usuarios(self):
        """Test de integración con API mockeada"""
        fake_response = {
            "users": [
                {"id": 1, "name": "Alice"},
                {"id": 2, "name": "Bob"}
            ]
        }
        
        with mock_api_response("*/users", fake_response):
            from tu_app import obtener_usuarios
            usuarios = obtener_usuarios()
            self.assertEqual(len(usuarios), 2)
            self.assertEqual(usuarios[0]['name'], 'Alice')

Bonus: Fixture con Timer

Útil para medir performance:

from contextlib import contextmanager
import time

@contextmanager
def timer(nombre_test):
    """Mide el tiempo de ejecución de un test"""
    inicio = time.time()
    try:
        yield
    finally:
        duracion = time.time() - inicio
        print(f"\n⏱️  {nombre_test}: {duracion:.4f}s")


def test_performance_critica(self):
    """Test con medición de tiempo"""
    with timer("Procesamiento de datos"):
        # Código a medir
        resultado = procesar_datos_grandes()
        self.assertIsNotNone(resultado)

Combinando con pytest (Bonus)

Si usas pytest, puedes convertir estos context managers en fixtures:

import pytest

@pytest.fixture
def archivo_json_temporal():
    """Fixture de pytest usando el context manager"""
    with temp_json_file({"test": "data"}) as path:
        yield path


def test_con_pytest(archivo_json_temporal):
    """Pytest usa el fixture automáticamente"""
    with open(archivo_json_temporal) as f:
        data = json.load(f)
    assert data["test"] == "data"

Ventajas de este Patrón

Reutilizable: Define una vez, usa en múltiples tests
Robusto: Cleanup garantizado incluso si el test falla
Legible: El código del test muestra claramente sus dependencias
Componible: Combina múltiples fixtures fácilmente
Mantenible: Cambios en el fixture afectan todos los tests automáticamente

Ejercicio Práctico

Crea un context manager fixture para:

  1. Mock de fecha/hora actual: Útil para tests que dependen del tiempo
  2. Directorio temporal con archivos: Para tests de sistemas de archivos
  3. Usuario de prueba: Para tests de autenticación
@contextmanager
def mock_current_time(year, month, day, hour=0, minute=0):
    """Tu implementación aquí"""
    pass

@contextmanager  
def temp_directory_with_files(file_structure):
    """
    Ejemplo: {"dir1/file.txt": "contenido", "dir2/data.json": {...}}
    """
    pass

@contextmanager
def test_user(username, role="user"):
    """Crea usuario de prueba y lo limpia"""
    pass

Conclusión

Este patrón de context managers como fixtures es un game changer para tests complejos. Lo vas a usar constantemente en proyectos reales donde necesitas:

  • Múltiples recursos temporales
  • Cleanup garantizado
  • Código de test limpio y expresivo
  • Reutilización máxima

¡Pruébalo en tu próximo proyecto y verás la diferencia! 🎯


Pro tip: Guarda tus fixtures en un archivo conftest.py (pytest) o test_fixtures.py (unittest) para reutilizarlos en todo tu proyecto.

Curso de Unit Testing en Python

Toma las primeras clases gratis

0 Comentarios

para escribir tu comentario

Artículos relacionados