¿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:
- Mock de fecha/hora actual: Útil para tests que dependen del tiempo
- Directorio temporal con archivos: Para tests de sistemas de archivos
- 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
COMPARTE ESTE ARTÍCULO Y MUESTRA LO QUE APRENDISTE




