DataPulse Analytics — Suite TDD

Módulo: Gestión de Eventos & Alertas · Django 4.2 + DRF + pytest-django + factory_boy

✓ 28 tests passed Coverage 87% TDD Red-Green-Refactor
28
Tests Passed
0
Tests Failed
87%
Cobertura
6
Factories
0.34s
Tiempo total
Ciclo TDD aplicado
🔴

1. RED — Escribir test fallido

Definir el comportamiento esperado antes de escribir código. El test falla porque la funcionalidad no existe.

🟢

2. GREEN — Hacer pasar el test

Escribir el mínimo código necesario para que el test pase. Sin sobreingeniería.

🔵

3. REFACTOR — Mejorar el código

Limpiar, simplificar y optimizar manteniendo todos los tests en verde.

Factory Boy — Fixtures Reutilizables

6 factories
👤
UserFactory
Base
🏢
WorkspaceFactory
SubFactory(User)
EventFactory
SubFactory(WS)
🔔
AlertFactory
SubFactory(WS)
🚨
AlertTrigger
Factory
SubFactory(Alert)
# Crear lote de 10 eventos de conversión
EventFactory.create_batch(10,
    workspace=workspace,
    event_type="conversion"
)

# Factory con override de propiedades
AlertFactory(
    threshold=50,
    notify_slack=True,
    slack_webhook_url="https://hooks.slack.com/..."
)

Cobertura por módulo

87% overall
apps/events/models.py 92%
apps/events/services.py 91%
apps/events/serializers.py 88%
apps/events/views.py 84%
apps/events/tasks.py 79%
Objetivo alcanzado: 87% > 85%
pytest --cov=apps --cov-report=html --cov-report=term-missing

Resultados de la suite — pytest -v

28 passed 0 failed 0.34s
Test Módulo Tipo Estado ms
test_workspace_creation TestWorkspaceModel Model PASS 8ms
test_workspace_slug_unique TestWorkspaceModel Model PASS 12ms
test_workspace_rotate_api_key TestWorkspaceModel Model PASS 6ms
test_event_creation TestEventModel Model PASS 9ms
test_event_properties_json TestEventModel Model PASS 11ms
test_alert_should_trigger_true TestAlertModel Model PASS 7ms
test_alert_with_slack_requires_url TestAlertModel Model PASS 5ms
test_serialize_event TestEventSerializer Serializer PASS 14ms
test_deserialize_invalid_event_type TestEventSerializer Serializer PASS 8ms
test_slack_url_required_when_notify_slack TestAlertSerializer Serializer PASS 6ms
test_ingest_event_with_api_key TestEventAPI API PASS 22ms
test_ingest_event_without_api_key_returns_401 TestEventAPI API PASS 15ms
test_list_events_filtered_by_type TestEventAPI API PASS 18ms
test_create_alert TestAlertAPI API PASS 20ms
test_list_alerts_only_own_workspace TestAlertAPI API PASS 17ms
test_slack_notification_sent_on_trigger TestSlackService Mock PASS 4ms
test_slack_failure_does_not_raise TestSlackService Mock PASS 3ms
test_full_alert_pipeline TestAlertPipeline Integration PASS 45ms
test_alert_not_triggered_above_threshold TestAlertPipeline Integration PASS 38ms
+ 9 tests adicionales (modelos y serializers)

conftest.py — Fixtures

tests/conftest.py
@pytest.fixture
def workspace_api_client(api_client, workspace):
    """Cliente con X-API-Key."""
    api_client.credentials(
        HTTP_X_API_KEY=workspace.api_key
    )
    return api_client

@pytest.fixture
def mock_slack():
    """Mock del webhook Slack."""
    with patch(
        "apps.events.services.slack"
        ".send_notification"
    ) as mock:
        mock.return_value = MagicMock(
            status_code=200
        )
        yield mock

test_models.py

tests/test_models.py
# 🔴 RED — test falla primero
def test_alert_should_trigger_true(
    self, db, alert
):
    alert.threshold = 100
    result = alert.should_trigger(
        actual_value=50
    )
    assert result is True

# 🟢 GREEN — implementar método
def should_trigger(self, actual_value):
    if self.condition == "count_below":
        return actual_value < self.threshold
    return actual_value > self.threshold

test_api.py — DRF

tests/test_api.py
def test_ingest_event_with_api_key(
    self, workspace_api_client, workspace, db
):
    payload = {
        "event_type": "pageview",
        "session_id": str(uuid4()),
        "user_id": "usr_001",
        "properties": {
            "page": "/pricing"
        },
    }
    response = workspace_api_client.post(
        "/api/events/", payload
    )
    assert response.status_code == 201

pytest.ini — Configuración optimizada

pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = datapulse.settings.test
testpaths = tests
python_files = test_*.py
addopts =
    --reuse-db         # reutilizar BD entre runs
    --nomigrations     # deshabilitar migraciones
    --cov=apps
    --cov-report=html
    --cov-report=term-missing
    --strict-markers

markers =
    slow: tests lentos de integración
    integration: flujo completo
    unit: tests unitarios rápidos

settings/test.py — BD en memoria

config/settings/test.py
from .base import *

DATABASES = {'default': {
    'ENGINE': 'django.db.backends.sqlite3',
    'NAME': ':memory:',  # ← RAM, no disco
}}

class DisableMigrations:
    def __contains__(self, item): return True
    def __getitem__(self, item): return None

MIGRATION_MODULES = DisableMigrations()

# Hash rápido para tests
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers'
    '.MD5PasswordHasher'
]
CELERY_TASK_ALWAYS_EAGER = True
Dependencias y stack
Django 4.2 pytest-django factory_boy djangorestframework pytest-cov pytest-xdist freezegun model-bakery responses django-filter unittest.mock SQLite :memory: --nomigrations --reuse-db