DÍA 02 — NIVEL BÁSICO
Configuración del
entorno de testing
Configura un entorno profesional con settings separados, base de datos en memoria, emails capturados y estructura de carpetas lista para equipos.
Resumen del Día 1
Un test es código que verifica tu código · Django usa una BD temporal (tu BD real nunca se toca) · Todo método con prefijo
test_ se ejecuta automáticamente · Usamos TestCase como clase base · Asserts principales: assertEqual, assertTrue, assertRaises
Teoría del Día
// por qué necesitamos configuración especial
¿Por qué necesitamos settings separados?
| ❌ Sin configuración especial | ✅ Con settings_test.py |
|---|---|
| Tests lentos — usan PostgreSQL real en disco | SQLite en memoria — 10x más rápido |
| Emails reales se envían a clientes | Emails capturados en memoria, nunca enviados |
| APIs externas de pago se llaman en cada test | APIs simuladas con mock |
| Logs llenan la consola innecesariamente | Logs silenciados durante los tests |
| El equipo tiene configuraciones distintas | Mismo entorno para todo el equipo |
Estructura de archivos que crearemos
estructura del proyecto
mi_tienda/ ├── manage.py ├── mi_tienda/ │ ├── settings.py ← ya existe (desarrollo) │ ├── settings_test.py ← ✨ NUEVO: solo para tests │ ├── urls.py │ └── wsgi.py ├── productos/ │ ├── models.py │ └── tests/ ← ✨ NUEVO: carpeta organizada │ ├── __init__.py ← ✨ vacío (hace la carpeta importable) │ ├── test_models.py │ └── test_views.py └── pytest.ini ← ✨ NUEVO: configuración de pytest
Demostración en Vivo
// código completo paso a paso
PASO 1 — Crear settings_test.py
mi_tienda/settings_test.py
from .settings import * # ↑ Importa TODO de settings.py # ↑ Solo sobreescribimos lo que necesita cambiar # ── BD EN MEMORIA ──────────────────────────────── # SQLite en memoria es ~10x más rápido que PostgreSQL DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', # ↑ ':memory:' = vive en RAM, no en disco # ↑ Se crea y destruye con cada ejecución } } # ── EMAILS — capturar en lugar de enviar ───────── EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' # ↑ Los emails se guardan en django.core.mail.outbox # ↑ NUNCA se envían emails reales durante tests # ── CONTRASEÑAS — hasher más rápido ────────────── PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', # ↑ MD5 es rápido (no seguro, pero OK para tests) # ↑ Bcrypt/argon2 son lentos por diseño (seguridad) ] # ── CACHÉ — desactivar para tests predecibles ──── CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', # ↑ DummyCache no guarda nada realmente # ↑ Evita que tests se afecten entre sí por caché } } # ── LOGGING — silenciar consola ─────────────────── LOGGING = { 'version': 1, 'disable_existing_loggers': True, # ↑ Silencia todos los loggers → consola limpia } # ── ARCHIVOS MEDIA — directorio temporal ───────── import tempfile MEDIA_ROOT = tempfile.mkdtemp() # ↑ Archivos subidos van a carpeta temporal # ── SECRET_KEY — fija para tests ───────────────── SECRET_KEY = 'clave-solo-para-tests-no-usar-en-produccion' # ── CELERY — síncrono en tests (Día 17) ────────── CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_EAGER_PROPAGATES = True
PASO 2 — setUp y tearDown: preparar y limpiar
setUp se ejecuta antes de cada test y tearDown después de cada test. Son el lugar correcto para preparar datos comunes y limpiar recursos externos.
productos/tests/test_configuracion.py
from django.test import TestCase from django.core import mail class SetUpTearDownTest(TestCase): def setUp(self): # ↑ Se ejecuta ANTES de cada test # ↑ Úsalo para preparar datos comunes self.precio_base = 100.0 self.descuento = 20 self.producto = "Laptop Gaming" def tearDown(self): # ↑ Se ejecuta DESPUÉS de cada test # ↑ Úsalo para limpiar archivos, conexiones, etc. pass # Aquí limpiarías recursos externos si los hubiera def test_usa_datos_del_setup(self): precio_final = self.precio_base * (1 - self.descuento / 100) self.assertEqual(precio_final, 80.0) def test_nombre_producto_no_vacio(self): # setUp se volvió a ejecutar → self.producto existe self.assertTrue(len(self.producto) > 0) class SetUpClassTest(TestCase): """setUpClass se ejecuta UNA sola vez para toda la clase""" @classmethod def setUpClass(cls): super().setUpClass() # ← SIEMPRE llama a super() primero cls.config = { 'moneda': 'USD', 'impuesto': 0.19, 'version_api': 'v2' } def test_moneda_correcta(self): self.assertEqual(self.config['moneda'], 'USD') def test_impuesto_correcto(self): self.assertEqual(self.config['impuesto'], 0.19) class EmailBackendTest(TestCase): """Verifica que los emails se capturan y no se envían""" def test_email_capturado_no_enviado(self): from django.core.mail import send_mail send_mail( subject='Confirmación pedido #1234', message='Tu pedido está en camino', from_email='tienda@rapidoya.com', recipient_list=['cliente@gmail.com'], ) self.assertEqual(len(mail.outbox), 1) # ↑ mail.outbox es la bandeja en memoria self.assertEqual( mail.outbox[0].subject, 'Confirmación pedido #1234' ) self.assertIn('cliente@gmail.com', mail.outbox[0].to)
PASO 3 — pytest.ini y comandos de ejecución
pytest.ini — raíz del proyecto
[pytest] # Settings a usar en todos los tests DJANGO_SETTINGS_MODULE = mi_tienda.settings_test # Dónde buscar los archivos de test testpaths = . # Patrón de nombres python_files = test_*.py python_classes = *Test python_functions = test_* # Opciones por defecto addopts = --verbose --tb=short
terminal — comandos esenciales
# Instalar dependencias de testing pip install pytest-django pytest coverage # Opción A: Django nativo con settings específicos python manage.py test --settings=mi_tienda.settings_test # Opción B: Variable de entorno (más cómodo) export DJANGO_SETTINGS_MODULE=mi_tienda.settings_test python manage.py test # Opción C: pytest (con pytest.ini configurado) pytest # todos los tests pytest productos/ # solo una app pytest -k "test_email" # tests con "email" en el nombre pytest -x # para en el primer fallo pytest --lf # solo los que fallaron la última vez
output esperado
$ pytest --verbose ===== test session starts ===== django: settings: mi_tienda.settings_test ← ✅ settings de test collected 5 items test_email_capturado_no_enviado .... PASSED test_impuesto_correcto ............. PASSED test_moneda_correcta ............... PASSED test_nombre_producto_no_vacio ...... PASSED test_usa_datos_del_setup ........... PASSED ===== 5 passed in 0.18s ===== # ↑ Rápido gracias a SQLite en memoria
3 errores comunes del Día 2
Olvidar
super().setUpClass() en setUpClass · Usar setUp para operaciones costosas que deberían ir en setUpClass · No crear el archivo __init__.py vacío en la carpeta tests/
Retos del Día
// practica lo aprendido
RETO 01
★☆☆
Configurar tu entorno de tests
Crear la estructura profesional de archivos para testing lista para un equipo real.
- 1Crea
settings_test.pycon BD en memoria (':memory:') - 2Crea la carpeta
productos/tests/ - 3Agrega
__init__.pyvacío dentro de la carpeta tests/ - 4Crea
test_dia2.pycon al menos 1 test que pase - 5Ejecuta con
--settings=mi_tienda.settings_testy verifica OK
Pista: El
__init__.py puede estar completamente vacío. Solo necesita existir para que Django encuentre los tests.
RETO 02
★★☆
setUp en acción
Usar setUp para eliminar código duplicado entre tests de un carrito de compras.
- 1Crea
CarritoTestcon un setUp que defina:self.productos,self.precios(lista) yself.descuento_vip = 15 - 2
test_carrito_tiene_3_productos: verificalen(self.productos) == 3 - 3
test_total_sin_descuento: suma de precios es correcta - 4
test_total_con_descuento_vip: aplica el 15% al total
Pista: El total sin descuento es
sum(self.precios). El total con descuento VIP es total * (1 - 0.15)
RETO 03
★★★
Sistema completo de notificaciones por email
Crear tests que verifiquen el sistema de notificaciones de un e-commerce usando el backend en memoria.
- 1Crea la función
enviar_confirmacion_pedido(email, numero, total)que envíe un email con asunto"Pedido #[numero] confirmado" - 2
test_se_envia_exactamente_un_email - 3
test_asunto_contiene_numero_pedido - 4
test_mensaje_contiene_total - 5
test_destinatario_correcto
Pista:
from django.core import mail → verifica mail.outbox después de llamar tu función. Cada test empieza con mail.outbox vacío automáticamente.Caso Práctico Real
// de 4 minutos a 0.031 segundos
ShopFast — Tests que tardan 4 minutos
// e-commerce · 50k usuarios · equipo de 8 devs
Contexto
Eres el nuevo dev en ShopFast. El equipo tiene tests pero todos usan la BD de desarrollo real. Los tests tardan 4 minutos en correr y a veces dejan datos sucios. El Tech Lead te pide migrar la configuración.
Requerimiento
Configurar el entorno para que use SQLite en memoria, capture emails, y tenga un setUp reutilizable para los tests de
pedidos. El equipo pierde 30 minutos al día esperando que corran los tests.Tu Tarea
Crear la configuración completa y demostrar que funciona con tests reales de notificaciones por email.
pedidos/tests/test_pedidos.py — Solución guiada
from django.test import TestCase from django.core import mail def crear_pedido_y_notificar(cliente_email, productos, total): """Simula crear pedido y enviar confirmación por email""" numero_pedido = 9999 mail.send_mail( subject=f'ShopFast — Pedido #{numero_pedido} recibido', message=f'Gracias por tu compra. Total: ${total}.', from_email='no-reply@shopfast.com', recipient_list=[cliente_email], ) return numero_pedido class PedidoNotificacionTest(TestCase): def setUp(self): self.email = 'juan@gmail.com' self.total = 1130.0 self.num = crear_pedido_y_notificar( self.email, ['Laptop', 'Mouse'], self.total ) def test_exactamente_un_email(self): self.assertEqual(len(mail.outbox), 1) def test_asunto_menciona_numero_pedido(self): self.assertIn(str(self.num), mail.outbox[0].subject) def test_mensaje_incluye_total(self): self.assertIn('1130', mail.outbox[0].body) def test_email_llega_al_cliente(self): self.assertIn(self.email, mail.outbox[0].to) def test_remitente_es_shopfast(self): self.assertEqual(mail.outbox[0].from_email, 'no-reply@shopfast.com')
resultado
test_asunto_menciona_numero_pedido ... ok test_email_llega_al_cliente ......... ok test_exactamente_un_email ........... ok test_mensaje_incluye_total .......... ok test_remitente_es_shopfast .......... ok Ran 5 tests in 0.031s ← ⚡ antes: 4 minutos OK — 30 min/día recuperados 🎉
Checklist de Aprendizaje
// marca lo que ya dominas
Haz clic en cada casilla para marcarla. Tu progreso se guarda automáticamente.
- Crear
settings_test.pyque hereda desettings.py - Configurar BD SQLite en memoria con
':memory:' - Entender por qué
':memory:'hace los tests más rápidos - Configurar
EMAIL_BACKENDpara capturar emails sin enviarlos - Organizar tests en carpeta
tests/con__init__.py - Usar
setUppara preparar datos antes de cada test - Usar
setUpClasspara datos costosos que se crean una sola vez - Ejecutar tests con
--settingsespecíficos - Verificar emails capturados con
mail.outbox - Conocer los flags útiles de pytest:
-x,--lf,-k