Saltar al contenido principal
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.

35 min de lectura
3 retos prácticos
1 caso real
Básico
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 discoSQLite en memoria — 10x más rápido
Emails reales se envían a clientesEmails capturados en memoria, nunca enviados
APIs externas de pago se llaman en cada testAPIs simuladas con mock
Logs llenan la consola innecesariamenteLogs silenciados durante los tests
El equipo tiene configuraciones distintasMismo entorno para todo el equipo

Estructura de archivos que crearemos

estructura del proyecto
ÁRBOL
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
Python
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
Python
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
INI
[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
BASH
# 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
OUTPUT
$ 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.py con BD en memoria (':memory:')
  • 2Crea la carpeta productos/tests/
  • 3Agrega __init__.py vacío dentro de la carpeta tests/
  • 4Crea test_dia2.py con al menos 1 test que pase
  • 5Ejecuta con --settings=mi_tienda.settings_test y 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 CarritoTest con un setUp que defina: self.productos, self.precios (lista) y self.descuento_vip = 15
  • 2test_carrito_tiene_3_productos: verifica len(self.productos) == 3
  • 3test_total_sin_descuento: suma de precios es correcta
  • 4test_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"
  • 2test_se_envia_exactamente_un_email
  • 3test_asunto_contiene_numero_pedido
  • 4test_mensaje_contiene_total
  • 5test_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
Python
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
OUTPUT
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.py que hereda de settings.py
  • Configurar BD SQLite en memoria con ':memory:'
  • Entender por qué ':memory:' hace los tests más rápidos
  • Configurar EMAIL_BACKEND para capturar emails sin enviarlos
  • Organizar tests en carpeta tests/ con __init__.py
  • Usar setUp para preparar datos antes de cada test
  • Usar setUpClass para datos costosos que se crean una sola vez
  • Ejecutar tests con --settings específicos
  • Verificar emails capturados con mail.outbox
  • Conocer los flags útiles de pytest: -x, --lf, -k