Módulo 5: Coverage y Edge Cases

Interpretar Coverage: Qué Significan los Números

Interpretar Coverage: Qué Significan los Números

Descripción de la cápsula

Ejecutas pytest --cov y ves 78% line coverage. ¿Eso es bueno o malo? ¿Deberías perseguir el 100%? Y si un módulo tiene 95% coverage pero falla en producción por un bug en un except que nunca fue testeado, ¿qué te dice realmente ese 95%?

Esta cápsula responde la pregunta más importante sobre coverage: los números no son la meta — son una guía. Coverage te indica dónde buscar tests faltantes. No te dice cuáles tests importan. 100% coverage con asserts triviales es inútil. 60% coverage con tests que verifican comportamiento crítico vale más que el 100% con padding.

Al final, sabrás interpretar un reporte de coverage línea por línea, distinguir gaps críticos de gaps aceptables, priorizar qué cubrir primero, y usar Claude Code para cerrar los gaps que realmente importan.


Los Números de Coverage No Son Números de Calidad

100% coverage con asserts triviales = inútil

Imagina esta suite:

# my_module.py
def calculate_invoice(items: list[dict], tax_rate: float) -> float:
    if not items:
        raise ValueError("Items cannot be empty")
    subtotal = sum(item["price"] * item["qty"] for item in items)
    return round(subtotal * (1 + tax_rate), 2)


# test_my_module.py (versión trivial)
def test_calculate_invoice_not_empty():
    result = calculate_invoice([{"price": 10, "qty": 2}], 0.1)
    assert result is not None

def test_calculate_invoice_returns_float():
    result = calculate_invoice([{"price": 100, "qty": 1}], 0.1)
    assert isinstance(result, float)

Coverage: 100%. Cada línea se ejecutó. Pero si alguien cambia la fórmula a return 0, los tests siguen pasando. El coverage miente: te dice que todo está cubierto, pero no verificaste el valor correcto.

60% coverage con tests significativos > 100% trivial

Compara con esta suite:

# test_my_module.py (versión significativa)
def test_calculate_invoice_correct_result():
    result = calculate_invoice([{"price": 100, "qty": 2}], 0.1)
    assert result == 220.0  # 200 * 1.1

def test_calculate_invoice_empty_raises():
    with pytest.raises(ValueError, match="Items cannot be empty"):
        calculate_invoice([], 0.1)

Coverage: quizá 60%. Pero si alguien rompe la fórmula o la validación, los tests fallan. Esos tests protegen contra bugs reales.

Regla de oro: Coverage te dice dónde van los tests. No te dice si esos tests verifican lo correcto.

El mental model correcto

Cuando lees un reporte de coverage, pregúntate:

  • "¿Este porcentaje refleja tests que detectarían bugs?" — No automáticamente.
  • "¿Dónde están los huecos más peligrosos?" — Ahí es donde el reporte ayuda.
  • "¿Vale la pena escribir un test para esta línea?" — Depende de la prioridad.

Coverage es una herramienta de descubrimiento, no una métrica de calidad final. Un equipo que entiende esto usa el reporte para priorizar trabajo. Un equipo que no lo entiende pierde tiempo persiguiendo números vanos.


Line Coverage vs Branch Coverage

El problema: una línea puede tener múltiples caminos

Considera esta función:

# calculator.py
def process(value: int) -> int:
    if value > 0:        # Branch 1a: True, Branch 1b: False
        return value * 2
    elif value == 0:     # Branch 2a: True, Branch 2b: False
        return 0
    else:
        return -value

Si escribes un solo test:

def test_process_positive():
    assert process(5) == 10

Ese test ejecuta:

  • Línea 2: if value > 0 → True
  • Línea 3: return value * 2
  • No ejecuta: línea 4 (elif), 5, 6, 7

Line coverage: 3 de 7 líneas ≈ 43%

Branch coverage: Cada if/elif/else crea ramas. Hay 6 ramas lógicas:

  1. value > 0 → True ✅
  2. value > 0 → False
  3. value == 0 → True
  4. value == 0 → False
  5. else (value < 0) → ejecutado
  6. else → no ejecutado (ya cubierto si llegas al else)

En la práctica, branch coverage cuenta: ¿cuántas ramas de decisión fueron tomadas? Con un solo test process(5), solo tomaste la rama "True" del primer if. Las otras ramas quedaron sin cubrir.

Branch coverage: ≈ 17% (1 de 6 ramas)

Si/else con solo el True cubierto = 50% branch coverage

def check_even(n: int) -> str:
    if n % 2 == 0:
        return "even"
    else:
        return "odd"

Un test con check_even(4) cubre 100% de líneas. Pero branch coverage: solo la rama True del if fue ejecutada. La rama False (else) no. Si la implementación tuviera un bug en el else, no lo detectarías.

Branch coverage revela caminos de lógica que no pensaste en testear.

Cómo ver branch coverage con pytest-cov

pytest --cov=my_module --cov-report=term-missing --cov-branch -v

El flag --cov-branch activa la medición de branch coverage. El reporte mostrará algo como:

Name           Stmts   Miss Branch BrPart  Cover
-----------------------------------------------
calculator.py      7      4      6      5    17%

BrPart = ramas parcialmente cubiertas. Branch = total de ramas.

Function coverage: ¿qué es y cuándo importa?

Además de line y branch, existe function coverage: qué porcentaje de funciones fueron llamadas al menos una vez. Es la métrica más débil: si una función tiene 100 líneas y solo llamaste la función una vez, tienes 100% function coverage pero quizá 10% line coverage. Suele ser útil como vista rápida ("¿hay funciones que nadie testea?"), pero no reemplaza a line/branch para tomar decisiones.


Qué Significan las Líneas No Cubiertas

No todas las líneas descubiertas son iguales. Hay cuatro categorías típicas:

1. Código de manejo de errores (except blocks)

def fetch_user(user_id: str) -> dict:
    try:
        return api.get(f"/users/{user_id}")
    except ConnectionError:
        logger.warning("API unavailable, using cache")
        return cache.get(user_id, {})
    except ValueError:
        raise

Los bloques except rara vez se ejecutan en tests normales. Si solo testeas el happy path, el coverage reporta esas líneas como "missed". Pero son críticas: en producción, los errores de red o validación ocurren. Si el fallback está mal implementado, tendrás bugs silenciosos.

2. Código de fallback o default

def get_theme(user) -> str:
    if user.preferences:
        return user.preferences.theme
    return "default"  # ← Línea no cubierta si solo testeas con user.preferences

El valor por defecto parece obvio, pero si alguien cambia "default" por "light" y hay código que depende del string exacto, un bug aparece. Los defaults y fallbacks merecen al menos un test.

3. Código muerto (dead code)

def calculate(x: int) -> int:
    if x < 0:
        return 0
    if False:  # ← Nunca se ejecuta
        return -1
    return x * 2

Líneas que nunca se ejecutan porque la condición es imposible o hay lógica redundante. No debes escribir tests para cubrirlas — debes eliminar ese código.

4. Condiciones complejas

if (a and b) or (c and not d):
    do_something()

Con múltiples combinaciones de a, b, c, d, es fácil que algunas ramas queden sin cubrir. Branch coverage te ayuda a identificar qué combinación falta.


Priorizando Qué Cubrir

No todas las líneas no cubiertas merecen la misma atención. Usa este framework: "¿Qué dolería más si este código tuviera un bug?"

Prioridad 1: Error handling (except, raise, error returns)

  • Bloques except que hacen fallback o retornan valor por defecto
  • raise de excepciones personalizadas
  • Respuestas de error en APIs (400, 404, 500)

Riesgo: Bugs en errores = fallos silenciosos o mensajes incorrectos en producción.

Prioridad 2: Business logic branches (if/else en funciones core)

  • Cálculos condicionales (descuentos, impuestos, límites)
  • Validaciones (rangos, tipos, formatos)
  • Flujos alternativos (pago vs. crédito, suscriptor vs. invitado)

Riesgo: Lógica incorrecta en una rama = datos erróneos o comportamientos inesperados.

Prioridad 3: Edge case paths (inputs vacíos, None, límites)

  • if not items, if value is None, if len(s) == 0
  • Valores en frontera: 0, -1, MAX_INT, string vacío

Riesgo: Crashes o resultados incorrectos con inputs inusuales.

Prioridad 4: Logging, formateo, utilidades cosméticas

  • logger.debug(...), logger.info(...)
  • Formateo de strings para mensajes
  • Helpers que solo reenvían a otras funciones

Riesgo: Bajo. Un bug aquí rara vez afecta el resultado correcto. Cubrir si sobra tiempo, pero no priorizar.

Checklist rápido para priorizar gaps

Cuando veas líneas "Missing" en el reporte, pregunta en orden:

  1. ¿Es un bloque except o raise? → Prioridad 1
  2. ¿Es un if/else en lógica core? → Prioridad 2
  3. ¿Es validación de input (vacío, None, límites)? → Prioridad 3
  4. ¿Es logging, formateo o getter trivial? → Prioridad 4
  5. ¿Parece imposible de ejecutar (dead code)? → Eliminar, no testear

Este checklist te ayuda a decidir en segundos si un gap merece un test o puede quedarse.


La Trampa del 100% Coverage

Perseguir 100% lleva a tests de implementación

Cuando el objetivo es "llegar a 100%", terminas escribiendo tests como:

def test_user_name_getter():
    u = User(name="Alice")
    assert u.name == "Alice"

def test_user_str():
    u = User(name="Alice")
    assert "Alice" in str(u)

Esos tests verifican implementación (getters, __str__), no comportamiento. Si cambias la implementación interna (por ejemplo, name pasa a ser una propiedad computada), el test rompe aunque el comportamiento sea el mismo. Tests frágiles, poco valor.

Getters, setters, str, logging → desperdicio de tiempo

  • Tests para @property que solo retornan un atributo
  • Tests para __str__ que formatean strings
  • Tests para funciones que solo llaman logger.debug

Mejor invertir ese tiempo en tests de integración o edge cases de lógica real.

Mejor: 90% con tests significativos que 100% con padding

Un módulo con 90% coverage donde:

  • Todos los paths de error importantes están cubiertos
  • La lógica de negocio tiene tests parametrizados
  • Los edge cases críticos están verificados

…vale más que un módulo con 100% coverage logrado añadiendo tests para __repr__ y logging.


Ejemplo Real: Analizar un Reporte de Coverage

El módulo bajo análisis

# pricing.py
import logging
logger = logging.getLogger(__name__)

def apply_discount(price: float, discount_pct: float, min_price: float = 0) -> float:
    """Apply discount. Returns discounted price or min_price if lower."""
    if price < 0:
        raise ValueError("Price cannot be negative")
    if not 0 <= discount_pct <= 100:
        raise ValueError("Discount must be between 0 and 100")
    discounted = price * (1 - discount_pct / 100)
    if discounted < min_price:
        logger.info(f"Capping at min_price={min_price}")
        return min_price
    return round(discounted, 2)


def validate_coupon(code: str) -> bool:
    """Validate coupon format. Real validation would call external API."""
    if not code or not code.strip():
        return False
    if len(code) < 5:
        return False
    # Dead code: esta condición nunca se cumple en la práctica actual
    if code == "REMOVED_COUPON":
        return False
    return True

Los tests actuales

# test_pricing.py
import pytest
from pricing import apply_discount, validate_coupon

def test_apply_discount_basic():
    assert apply_discount(100, 10) == 90.0

def test_apply_discount_zero_discount():
    assert apply_discount(50, 0) == 50.0

def test_validate_coupon_valid():
    assert validate_coupon("SAVE20") is True

Reporte de coverage (simplificado)

Name        Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------
pricing.py     18      6     10      7    67%   5-8, 11-14, 17, 23-24

Líneas no cubiertas:

  • 5-8: Validaciones (price < 0, discount_pct fuera de rango) — Prioridad 1
  • 11-14: Bloque if discounted < min_price — Prioridad 2
  • 17: Implícito: el return round(...) cuando no entra al if — cubierto por test básico, pero el min_price default nunca se usa en el camino cubierto
  • 23-24: validate_coupon con code vacío o muy corto — Prioridad 3
  • Línea del dead code (code == "REMOVED_COUPON") — No priorizar, eliminar código muerto

Decisión: qué cerrar y qué aceptar

  • ✅ Cerrar: Tests para apply_discount con price < 0 y discount_pct inválido (Prioridad 1)
  • ✅ Cerrar: Test para apply_discount con min_price cuando discounted < min_price (Prioridad 2)
  • ✅ Cerrar: Tests para validate_coupon con "", " ", "abc" (Prioridad 3)
  • ❌ No cerrar con test: La línea del code == "REMOVED_COUPON" — eliminar o documentar como legacy
  • ❌ Aceptar sin test: El logger.info — Prioridad 4, bajo riesgo

Antes y después: el reporte tras cerrar los gaps prioritarios

Antes (67%):

pricing.py     18      6     10      7    67%   5-8, 11-14, 17, 23-24

Después de añadir los tests recomendados (Prioridad 1, 2, 3):

pricing.py     18      2      10      3    89%   19-20, 27

Lo que queda sin cubrir: el logger.info (Prioridad 4) y el dead code de REMOVED_COUPON. Has pasado de 67% a 89% cubriendo solo lo que importa. El resto puedes aceptarlo o eliminar el dead code en un refactor.


Usar Claude Code para Cerrar Gaps de Coverage

El prompt efectivo

Puedes dar a Claude Code el reporte y pedirle tests específicos:

Aquí está mi reporte de coverage. Las líneas 5-8 y 11-14 de pricing.py no están cubiertas.

Código de pricing.py:
[pega el código]

Tests actuales:
[pega test_pricing.py]

Genera tests que cubran esas líneas. Los tests deben verificar comportamiento, no solo ejecutar código.
Usa pytest y pytest.raises para las excepciones.

Respuesta típica de Claude Code

def test_apply_discount_negative_price_raises():
    with pytest.raises(ValueError, match="Price cannot be negative"):
        apply_discount(-10, 5)

def test_apply_discount_invalid_discount_raises():
    with pytest.raises(ValueError, match="Discount must be between 0 and 100"):
        apply_discount(100, 150)

def test_apply_discount_caps_at_min_price():
    result = apply_discount(100, 90, min_price=15)
    assert result == 15  # 100 * 0.1 = 10, pero min_price es 15

Verificar el nuevo coverage

pytest test_pricing.py --cov=pricing --cov-report=term-missing

Deberías ver que las líneas 5-8 y 11-14 pasan de "Missing" a cubiertas.

Variaciones del prompt para diferentes escenarios

  • Para error handling: "Las líneas X-Y son un bloque except. Genera un test que fuerce esa excepción (mock, input inválido, etc.) y verifique el fallback."

  • Para branch coverage: "La rama False del if en línea N no está cubierta. Genera un test con input que tome esa rama y verifique el valor de retorno."

  • Para edge cases: "La función valida inputs. Genera tests parametrizados para: vacío, None, cero, negativo, string muy largo."

Cada tipo de gap tiene un prompt más efectivo. Cuanto más específico seas sobre qué cubrir y qué verificar, mejor será la salida de Claude Code.


Checklist Práctico para Interpretar Reportes

Cuando abras un reporte de coverage, sigue estos pasos en orden:

  1. Ejecuta con branch coverage:

    pytest --cov=tu_modulo --cov-branch --cov-report=term-missing -v

    Sin --cov-branch pierdes información crítica sobre ramas no cubiertas.

  2. Lista las líneas "Missing" por archivo — no intentes cubrirlas todas. Clasifícalas en:

    • Prioridad 1 (error handling)
    • Prioridad 2 (lógica de negocio)
    • Prioridad 3 (edge cases)
    • Prioridad 4 o "eliminar" (logging, dead code)
  3. Para cada línea Prioridad 1 o 2: Pregúntate "¿Qué pasaría si esta línea tuviera un bug?" Si la respuesta es "fallo silencioso", "dato incorrecto" o "crash en producción", escribe o genera el test.

  4. Revisa el branch coverage: Si BrPart (branches parciales) es alto, tienes muchos if/else donde solo cubriste una rama. Identifica la rama faltante y un input que la ejecute.

  5. No persigas 100% a menos que tu equipo tenga una política explícita. Un target de 85-90% con priorización inteligente suele ser más saludable que 100% con tests de relleno.

  6. Usa Claude Code para gaps concretos: En vez de "mejora el coverage", di "las líneas 14-16 de validators.py no están cubiertas. Es un bloque except. Genera un test que fuerce esa excepción y verifique el valor de retorno del fallback."

Este checklist te transforma de "no sé qué hacer con este 67%" a "tengo un plan de acción claro".

Cuándo NO escribir tests para cerrar coverage

Hay líneas que no merecen un test. Identificarlas te ahorra tiempo y mantiene la suite limpia:

  • Código muerto verificable: Si una rama es imposible (ej. if False), elimínala. No tests.
  • Logging puro: logger.debug(...), logger.info(...) sin lógica — Prioridad 4, omite a menos que tengas política estricta.
  • Métodos delegados: Si def get_x(self): return self._x solo reenvía, el test del caller ya cubre el comportamiento. Tests adicionales son redundantes.
  • Código generado o boilerplate: Serializers, DTOs que solo mapean campos — el valor de testearlos es bajo.
  • Política de equipo: Si el equipo acordó 85% como target y ya lo alcanzaste en los módulos críticos, no persigas más por número.

La clave: cada test debe tener un motivo. "Para subir el coverage" no es motivo suficiente si el test no detectaría un bug relevante.

Integración con el flujo TDD

Cuando trabajas con TDD + Claude Code, el coverage no reemplaza el ciclo red-green-refactor. Lo complementa:

  • Durante desarrollo: Escribes specs (tests) primero. El coverage inicial de la feature será alto porque los tests definieron el comportamiento.
  • Después de refactors: El coverage te revela si eliminaste tests o si hay ramas nuevas que surgieron del refactor.
  • En código legacy: El coverage es tu punto de partida: mides primero, identificas gaps, generas tests con Claude Code.

No uses coverage como excusa para escribir tests después. En código nuevo, TDD sigue siendo el flujo preferido. El coverage entra cuando tienes código existente o cuando quieres validar que no dejaste gaps en un refactor grande.


Ejercicios

Ejercicio 1: Calcular line vs branch coverage (Fácil)

Dada esta función, ¿qué porcentaje aproximado de line coverage y branch coverage obtienes si solo tienes un test con categorize(5)?

def categorize(value: int) -> str:
    if value > 10:
        return "high"
    elif value > 0:
        return "low"
    else:
        return "zero"
Ver solución
  • Line coverage: 4 de 7 líneas ≈ 57% (se ejecutan: if, elif True, return "low")
  • Branch coverage: 2 de 6 ramas ≈ 33% (solo value > 10 False, value > 0 True)

Para 100% branch coverage necesitas al menos: categorize(15), categorize(5), categorize(-3).


Ejercicio 2: Identificar prioridad de gaps (Fácil)

Clasifica cada línea no cubierta como Prioridad 1, 2, 3 o 4:

def process_order(order: dict) -> dict:
    if not order.get("items"):
        raise ValueError("Order must have items")
    total = sum(i["price"] * i["qty"] for i in order["items"])
    try:
        tax = external_api.get_tax(total)
    except TimeoutError:
        logger.warning("Tax API timeout, using 0")
        tax = 0
    return {"total": total, "tax": tax}

Líneas no cubiertas: 2 (raise), 7-8 (except block).

Ver solución
  • Línea 2 (raise ValueError): Prioridad 2 — lógica de negocio/validación
  • Líneas 7-8 (except TimeoutError): Prioridad 1 — error handling crítico (fallback en fallo de API)

Orden recomendado para cerrar: primero el except (Prioridad 1), luego el raise (Prioridad 2).


Ejercicio 3: Escribir tests para aumentar branch coverage (Medio)

Esta función tiene 50% branch coverage con un solo test. Escribe los tests faltantes para llegar a 100% branch coverage.

def parse_level(level: str) -> int:
    if level == "debug":
        return 10
    elif level == "info":
        return 20
    elif level == "warning":
        return 30
    else:
        return 0
Ver solución
import pytest
from my_module import parse_level

@pytest.mark.parametrize("level,expected", [
    ("debug", 10),
    ("info", 20),
    ("warning", 30),
    ("error", 0),
    ("", 0),
    ("unknown", 0),
])
def test_parse_level_all_branches(level, expected):
    assert parse_level(level) == expected

O bien tests individuales para cada rama. Lo importante: cubrir "debug", "info", "warning" y al menos un caso del else.


Ejercicio 4: Analizar reporte y priorizar (Medio)

Dado este reporte:

pricing.py     Stmts  Miss  Cover   Missing
                 22     5    77%    9, 12-13, 19-20

Y el código (fragmento):

# Línea 9: raise ValueError("Invalid")
# Líneas 12-13: except ValueError: return default
# Líneas 19-20: logger.debug("..."); helper()

¿Qué gaps cierras primero y cuáles aceptas sin cubrir?

Ver solución
  • Línea 9 (raise): Prioridad 2 — cerrar. Test con pytest.raises(ValueError).
  • Líneas 12-13 (except): Prioridad 1 — cerrar. Test que fuerce el ValueError y verifique el default.
  • Líneas 19-20 (logging + helper): Prioridad 4 — aceptar sin cubrir. O cerrar solo si helper() tiene lógica importante; si es puro logging, omitir.

Orden: 12-13 → 9 → (19-20 opcional).


Ejercicio 5: Prompt para Claude Code (Medio)

Redacta un prompt que le pidas a Claude Code para generar tests que cubran las líneas 14-16 de este módulo:

# validators.py
def validate_email(email: str) -> bool:
    if not email or "@" not in email:
        return False
    local, domain = email.split("@", 1)
    if len(local) < 2 or "." not in domain:  # Líneas 6-7
        return False
    return True
Ver solución
Las líneas 6-7 de validators.py (validators.py) no están cubiertas. Esa rama se ejecuta cuando local tiene menos de 2 caracteres O domain no contiene ".".

Código:
[pega validators.py]

Genera tests parametrizados con pytest que cubran esas líneas. Los casos deben incluir:
- email con local de 1 carácter
- email con domain sin punto

Cada test debe usar assert sobre el valor de retorno (True/False).

Ejercicio 6: Detectar dead code (Difícil)

En este fragmento hay una línea que nunca se ejecuta. Identifícala y explica por qué.

def get_status(code: int) -> str:
    if code >= 200 and code < 300:
        return "ok"
    if code >= 400:
        return "error"
    if code >= 300 and code < 400:
        return "redirect"
    return "unknown"  # ¿Cuándo se ejecuta?
Ver solución

La línea return "unknown" es dead code dado el flujo actual. Razón:

  • code >= 200 and code < 300 → return "ok"
  • code >= 400 → return "error"
  • code >= 300 and code < 400 → return "redirect"

¿Qué enteros quedan? Los < 200 (100-199, 0-99). Esos caerían en "unknown". Pero si la función solo recibe códigos HTTP típicos (200-599), en la práctica nunca llegarías ahí. Depende del contrato: si la función acepta cualquier int, "unknown" sí se ejecuta para códigos < 200. Si solo acepta 200-599, entonces "unknown" es dead code para el dominio real.

Para confirmar: ejecutar con get_status(199) o get_status(0). Si el contrato dice "solo HTTP válidos", considerar eliminar la rama o convertirla en raise ValueError("Invalid HTTP code").


Troubleshooting

Problema 1: El reporte muestra 0% branch coverage

Causa: No activaste --cov-branch.

Solución:

pytest --cov=my_module --cov-branch --cov-report=term-missing

En pyproject.toml o setup.cfg:

[tool.pytest.ini_options]
addopts = --cov=src --cov-branch --cov-report=term-missing

Problema 2: Coverage incluye archivos que no quieres (venv, tests)

Causa: Por defecto, coverage mide todo lo que se importa.

Solución: Configurar omit en .coveragerc o pyproject.toml:

[run]
omit =
    tests/*
    venv/*
    */__pycache__/*

Problema 3: Líneas marcadas como "missing" pero sí las ejecuté en el test

Causa: A veces coverage no detecta ejecución por saltos de línea o optimizaciones.

Solución: Verificar que el test realmente llama al camino esperado. Añade un print temporal o un breakpoint() para confirmar. Si el test está en otro proceso (p.ej. multiprocessing), coverage podría no contarlo — ejecuta los tests en el mismo proceso.


Problema 4: Branch coverage muy bajo aunque line coverage es alto

Causa: Muchos if/else donde solo testeaste la rama True (o la más común).

Solución: Revisar el reporte con --cov-report=term-missing y buscar "partial" branches. Para cada if, asegura al menos un test con condición True y otro con False. Usa @pytest.mark.parametrize para cubrir varias combinaciones.


Problema 5: Tests generados por Claude Code cubren líneas pero no verifican comportamiento

Causa: Prompts vagos como "genera tests para cubrir este código".

Solución: Ser explícito en el prompt:

Genera tests que:
1. Cubran las líneas X-Y
2. Verifiquen el valor exacto esperado (no assert result is not None)
3. Usen pytest.raises para excepciones
4. Incluyan al menos un edge case por rama

Conexión con Proyecto

El proyecto del módulo (cápsula 06) consiste en llevar un módulo existente a ≥90% coverage. Esta cápsula te da el criterio para:

  • Interpretar el reporte de coverage y decidir qué gaps cerrar primero
  • No perseguir 100% a costa de tests triviales
  • Priorizar error handling y lógica de negocio sobre logging y formateo
  • Usar Claude Code con prompts específicos para generar tests que cubran líneas concretas y verifiquen comportamiento

Flujo recomendado: medir → identificar gaps críticos (Prioridad 1 y 2) → generar tests con Claude Code → medir de nuevo → iterar.

Próxima cápsula: La cápsula 04 cubre la identificación y diseño de edge cases con Claude Code.


Integración con tu workflow diario

Una vez que interiorices la priorización (error handling → lógica → edge cases → logging), el reporte de coverage deja de ser un número que perseguir y pasa a ser una lista de tareas priorizada. Cada sesión de coding puede incluir: "hoy cierro los gaps Prioridad 1 de auth.py". Claude Code acelera la generación; tú decides qué gaps importan.

La combinación ganadora: tú interpretas, Claude Code implementa. Tú identificas que las líneas 23-25 son un bloque except crítico. Le das a Claude Code el contexto y el prompt específico. Recibes tests que cubren esas líneas y verifican el fallback. Ejecutas pytest --cov, confirmas que el gap se cerró, y pasas al siguiente. Ese loop — interpretar, priorizar, generar, verificar — es el núcleo de un coverage profesional con AI.


Resumen

  • ✅ Coverage indica dónde buscar tests, no si los tests son buenos
  • ✅ 100% coverage con asserts triviales es poco útil; 60% con tests significativos vale más
  • ✅ Branch coverage revela ramas de lógica no cubiertas (if/else)
  • ✅ Líneas no cubiertas: error handling (crítico), lógica de negocio, edge cases, logging (baja prioridad), dead code (eliminar)
  • ✅ Priorizar: error handling → lógica de negocio → edge cases → logging
  • ✅ Perseguir 100% lleva a tests de implementación; mejor 90% con tests que aporten valor
  • ✅ Claude Code puede generar tests para gaps concretos con prompts que pidan verificación de comportamiento, no solo ejecución

Recursos Adicionales

  1. Coverage.py - Branch Coverage — Documentación oficial de branch coverage
  2. Martin Fowler: Test Coverage — Por qué coverage no mide calidad
  3. pytest-cov Usage — Configuración y opciones
  4. The Pragmatic Programmer: Good Enough Software — Perspectiva sobre "good enough" en métricas
  5. Test Coverage Best Practices — Cuándo y cómo usar coverage
  6. Mutation Testing with mutmut — Herramienta para detectar tests que no verifican comportamiento

Módulo 5, Cápsula 03 — Testing with Claude Code Guide