Módulo 8: Proyecto Integrador — Test Suite Completa con TDD
Business Logic, Coverage y Edge Cases
Business Logic, Coverage y Edge Cases
Descripción de la cápsula
El core de TaskFlow API ya funciona: auth, equipos, tareas CRUD. Pero una aplicación profesional no solo hace lo básico — valida reglas de negocio, maneja edge cases, y expone estadísticas útiles. Esta cápsula cierra esos gaps usando TDD: implementas transiciones de estado válidas, permisos por rol, estadísticas de equipo, y usas hypothesis para property-based tests que descubren casos que no habías pensado.
Al terminar tendrás cobertura ≥90% y confianza en que las reglas de negocio están correctamente implementadas y testeadas.
Reglas de Negocio a Implementar
1. Transiciones de estado
Las tareas tienen estados: pending, in_progress, completed. No todas las transiciones son válidas:
pending→in_progress: válidain_progress→completed: válidacompleted→in_progress: inválida (no retroceder)pending→completed: inválida (debe pasar por in_progress)- Cualquier transición a un status inexistente: inválida
2. Permisos
- Solo el owner del equipo puede agregar miembros (ya cubierto en Teams)
- Solo el usuario asignado puede marcar una tarea como
completed - Tarea sin asignar: cualquier miembro del equipo puede completarla
3. Estadísticas de equipo
- Conteo de tareas por status (pending, in_progress, completed)
- Tasa de completación:
completed / total * 100
Implementar con TDD: Transiciones de Estado
Ciclo 1: Definir las reglas
Tests en tests/unit/test_rules.py:
# tests/unit/test_rules.py
from app.tasks.rules import can_transition, VALID_TRANSITIONS
def test_pending_to_in_progress_is_valid():
assert can_transition("pending", "in_progress") is True
def test_in_progress_to_completed_is_valid():
assert can_transition("in_progress", "completed") is True
def test_completed_to_in_progress_is_invalid():
assert can_transition("completed", "in_progress") is False
def test_pending_to_completed_is_invalid():
assert can_transition("pending", "completed") is False
def test_invalid_from_status_raises():
with pytest.raises(ValueError, match="Unknown status"):
can_transition("invalid", "pending")
def test_invalid_to_status_raises():
with pytest.raises(ValueError, match="Unknown status"):
can_transition("pending", "done")
Prompt a Claude Code:
Implementa app/tasks/rules.py con:
- can_transition(from_status, to_status) -> bool
- Transiciones válidas: pending->in_progress, in_progress->completed
- Cualquier otra transición retorna False
- Status desconocido: raise ValueError("Unknown status")
- Estados válidos: pending, in_progress, completed
Ciclo 2: Integrar en TaskService
Tests en tests/unit/test_tasks.py:
def test_update_task_status_valid_transition_succeeds():
service = TaskService()
task = service.create_task("T1", team_id=1, created_by=1, status="pending")
updated = service.update_task(task["id"], status="in_progress")
assert updated["status"] == "in_progress"
def test_update_task_status_invalid_transition_raises():
service = TaskService()
task = service.create_task("T1", team_id=1, created_by=1, status="completed")
with pytest.raises(ValueError, match="Invalid transition"):
service.update_task(task["id"], status="in_progress")
Prompt a Claude Code:
Modifica TaskService.update_task para validar transiciones de estado.
- Antes de actualizar status, llama can_transition(from, to)
- Si inválida: raise ValueError("Invalid transition")
- Usa app.tasks.rules.can_transition
Implementar con TDD: Permisos
Ciclo 3: Solo asignee puede completar
Tests en tests/unit/test_rules.py:
def test_only_assigned_user_can_complete_task():
# Lógica: si assigned_to existe y no es el usuario actual, no puede completar
from app.tasks.rules import can_user_complete_task
assert can_user_complete_task(assigned_to=5, current_user_id=5) is True
assert can_user_complete_task(assigned_to=5, current_user_id=3) is False
def test_unassigned_task_any_member_can_complete():
from app.tasks.rules import can_user_complete_task
assert can_user_complete_task(assigned_to=None, current_user_id=1) is True
Prompt a Claude Code:
Implementa can_user_complete_task(assigned_to, current_user_id) en app/tasks/rules.py:
- Si assigned_to is None: cualquier usuario puede completar (return True)
- Si assigned_to == current_user_id: puede completar (return True)
- Si assigned_to != current_user_id: no puede (return False)
Ciclo 4: Integrar permiso en update
Tests:
def test_non_assigned_user_cannot_complete_task():
service = TaskService()
task = service.create_task("T1", team_id=1, created_by=1, status="in_progress")
service.assign_task(task["id"], user_id=5) # Asignar a usuario 5
with pytest.raises(PermissionError, match="Only assigned user"):
service.update_task(task["id"], status="completed", updated_by=3) # Usuario 3 intenta
Prompt a Claude Code:
Modifica TaskService.update_task:
- Cuando status cambia a "completed", verifica can_user_complete_task(assigned_to, updated_by)
- Si False: raise PermissionError("Only assigned user can complete this task")
- updated_by es el user_id del usuario que hace la actualización
Implementar con TDD: Estadísticas
Ciclo 5: Team stats
Tests en tests/unit/test_rules.py o tests/unit/test_tasks.py:
def test_get_team_stats_returns_tasks_by_status():
from app.tasks.service import TaskService
service = TaskService()
service.create_task("T1", team_id=1, created_by=1, status="pending")
service.create_task("T2", team_id=1, created_by=1, status="in_progress")
service.create_task("T3", team_id=1, created_by=1, status="completed")
service.create_task("T4", team_id=1, created_by=1, status="completed")
stats = service.get_team_stats(team_id=1)
assert stats["by_status"]["pending"] == 1
assert stats["by_status"]["in_progress"] == 1
assert stats["by_status"]["completed"] == 2
assert stats["total"] == 4
def test_get_team_stats_completion_rate():
service = TaskService()
service.create_task("T1", team_id=1, created_by=1, status="pending")
service.create_task("T2", team_id=1, created_by=1, status="completed")
stats = service.get_team_stats(team_id=1)
assert stats["completion_rate"] == 50.0 # 1/2 * 100
def test_get_team_stats_empty_team():
service = TaskService()
stats = service.get_team_stats(team_id=999)
assert stats["total"] == 0
assert stats["completion_rate"] == 0.0
Prompt a Claude Code:
Agrega get_team_stats(team_id) a TaskService:
- Retorna { "by_status": {"pending": n, "in_progress": n, "completed": n}, "total": n, "completion_rate": float }
- completion_rate = (completed / total * 100) si total > 0, else 0
Medir Coverage con pytest-cov
Configuración en pyproject.toml
[tool.coverage.run]
source = ["app"]
omit = ["app/__init__.py", "tests/*"]
[tool.coverage.report]
fail_under = 90
show_missing = true
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError"
]
Comando
pytest tests/ -v --cov=app --cov-report=term-missing --cov-report=html
Interpretar el reporte
- Line coverage: porcentaje de líneas ejecutadas
- term-missing: muestra qué líneas no están cubiertas
- Target: ≥90% en
app/
Prompts para Edge Case Discovery con Claude Code
Prompt 1: Análisis de gaps
Revisa app/auth/service.py y app/tasks/service.py.
Lista todos los edge cases que podrían fallar y que no están cubiertos por los tests actuales.
Para cada uno, sugiere el nombre de un test que lo cubriría.
Prompt 2: Generar tests para un módulo
Genera 5 tests adicionales para app/tasks/rules.py que cubran edge cases.
Incluye: status vacío, None como assigned_to, completion_rate con total=0, transición al mismo status.
Prompt 3: Boundary testing
¿Qué valores límite podrían romper get_team_stats o can_transition?
Genera tests con pytest.mark.parametrize para esos casos.
Property-Based Testing con Hypothesis
Instalación
pip install hypothesis
Tests con hypothesis en test_rules.py
from hypothesis import given, strategies as st
@given(
from_s=st.sampled_from(["pending", "in_progress", "completed"]),
to_s=st.sampled_from(["pending", "in_progress", "completed"])
)
def test_can_transition_property_based(from_s, to_s):
"""Cualquier transición debe ser consistente: o True o False, nunca excepción inesperada."""
result = can_transition(from_s, to_s)
assert isinstance(result, bool)
if from_s == to_s:
# Transición a mismo estado: podría ser True o False según tu spec
pass # Solo verificamos que no crashea
elif (from_s, to_s) in [("pending", "in_progress"), ("in_progress", "completed")]:
assert result is True
else:
assert result is False
Otro ejemplo: completion_rate
@given(
pending=st.integers(min_value=0, max_value=100),
in_progress=st.integers(min_value=0, max_value=100),
completed=st.integers(min_value=0, max_value=100)
)
def test_completion_rate_always_between_0_and_100(pending, in_progress, completed):
total = pending + in_progress + completed
if total == 0:
rate = 0.0
else:
rate = (completed / total) * 100
assert 0 <= rate <= 100
Checklist de Coverage ≥90%
Antes de dar por cerrada esta cápsula:
-
pytest tests/ --cov=app --cov-fail-under=90pasa - Todos los módulos de
app/tienen tests unit -
app/tasks/rules.pytiene tests explícitos + hypothesis - Edge cases documentados: transiciones inválidas, permisos, stats vacíos
- No hay "pragma: no cover" innecesarios — si algo no se testea, pregúntate por qué
Errores Comunes
Error 1: Tests que pasan pero coverage baja
Causa: Tests superficiales que no ejecutan ramas condicionales (if/else, excepciones).
Solución: Revisa --cov-report=term-missing y escribe tests que ejerciten las líneas faltantes.
Error 2: Hypothesis encuentra contraejemplos
Causa: La propiedad que definiste es incorrecta o la implementación tiene un bug.
Solución: Hypothesis te dará el ejemplo que falla. Analízalo: ¿el test está mal o la implementación? Corrige lo que corresponda.
Error 3: Demasiados "pragma: no cover"
Causa: Código difícil de testear (logging, debug) marcado como no cover.
Solución: Solo usa pragma en código realmente no testeable. Si es lógica de negocio, debe tener test.
Resumen
- Implementas reglas de negocio con TDD: transiciones de estado, permisos, estadísticas
- Mides coverage con pytest-cov; target ≥90%
- Usas prompts a Claude Code para descubrir edge cases no considerados
- Usas hypothesis para property-based tests que validan invariantes
- La combinación de tests explícitos + property-based da confianza profesional
Próxima cápsula: Mocks, fixtures y CI pipeline — cerrar la infraestructura de testing.
Módulo 8, Cápsula 04 — Testing with Claude Code Guide