Módulo 5: Coverage y Edge Cases
Edge Case Discovery con Claude Code
Edge Case Discovery con Claude Code
Descripción de la cápsula
Escribiste tests para tu función. Cubren el happy path. Pero los bugs en producción casi nunca vienen del happy path — vienen del string vacío que nunca imaginaste, del None que "nunca debería llegar", del emoji en medio del username que rompe el parser. Los edge cases son invisibles para ti porque tu cerebro tiene sesgos que la IA no tiene.
Esta cápsula es la clave del Módulo 5: aquí es donde Claude Code brilla. No como reemplazo de tu pensamiento, sino como amplificador. Tú defines las categorías; Claude Code las llena con casos concretos que nunca habrías considerado. Aprenderás un marco sistemático de 10 categorías de edge cases, prompts específicos para descubrirlos, y cómo evaluar y priorizar los que realmente importan.
Por Qué los Humanos Pasamos por Alto Edge Cases
Sesgos cognitivos al testear
Cuando piensas en "qué testear" para una función, tu mente sigue patrones predecibles:
- Happy path bias: Diseñas para el éxito. La función "debería recibir datos válidos" — ¿pero qué pasa cuando no los recibe?
- Familiarity bias: Testeas con inputs que has visto antes.
"https://example.com","user@email.com",42. ¿Y el dominio con caracteres Unicode? ¿El email con 300 caracteres? - Normal path fixation: Piensas en el flujo que escribiste, no en los flujos que evitaste escribir.
La IA no tiene estos sesgos. Puede generar sistemáticamente categorías completas de casos que tu intuición descarta como "eso nunca pasa" — y que en producción sí pasan.
La trampa del "nunca va a pasar"
def get_username_from_email(email: str) -> str:
"""Extract username before @."""
return email.split("@")[0]
Tú pensarías en: "john@example.com" → "john". Tal vez "a@b.c" → "a".
Un atacante o un usuario en un edge case enviaría:
""→[""][0]=""(¿válido?)"no-at-sign"→["no-at-sign"][0]="no-at-sign"(¿es un username válido?)"@only-domain.com"→["", "only-domain.com"][0]="""user@with@two@ats.com"→["user", "with", "two", "ats.com"][0]="user"(¿correcto?)
La función "funciona" para tu caso mental. Falla en edge cases que nunca consideraste.
El rol de Claude Code
Claude Code puede:
- ✅ Recorrer sistemáticamente categorías de edge cases
- ✅ No tener el sesgo de "eso no pasa"
- ✅ Generar inputs adversariales que tú no imaginarías
- ✅ Documentar el por qué cada edge case importa
Tu trabajo: definir las categorías, filtrar lo que vale la pena, y priorizar. El de Claude Code: amplificar tu capacidad de descubrimiento.
Las 10 Categorías de Edge Cases
Usa este marco como checklist cuando pidas a Claude Code que descubra edge cases.
| Categoría | Ejemplos |
|---|---|
| Empty/Null | "", None, [], {} |
| Boundary values | 0, -1, MAX_INT, MIN_INT |
| Type mismatches | int donde se espera str, float donde se espera int |
| Unicode/Special chars | emojis, acentos, texto RTL, null bytes |
| Very large inputs | string de 10MB, lista con 1M items |
| Very small inputs | un solo carácter, lista de un solo elemento |
| Duplicate values | mismo item dos veces, claves duplicadas |
| Ordering | sorted, reverse sorted, un solo elemento, ya ordenado |
| Concurrency | misma operación dos veces simultáneamente |
| Format edge cases | espacios finales, mayúsculas/minúsculas mezcladas, caracteres especiales en paths |
Aplicación práctica
Para una función que procesa una lista de IDs:
def deduplicate_ids(ids: list[int]) -> list[int]:
"""Remove duplicates preserving order."""
seen = set()
result = []
for i in ids:
if i not in seen:
seen.add(i)
result.append(i)
return result
Recorriendo las categorías:
- Empty/Null:
[],[None](si la función aceptara otros tipos) - Boundary:
[0],[sys.maxsize],[-1] - Type mismatches:
["123", 123]si la firma fuera más flexible - Ordering:
[3, 2, 1],[1, 1, 1],[1] - Duplicate values:
[1, 2, 1, 2],[1, 1, 1, 1]
Claude Code puede expandir cada categoría con casos concretos para tu función específica.
Prompts para Descubrir Edge Cases
1. Genérico
¿Qué edge cases no estoy testeando para esta función?
Resultado típico: Respuesta amplia pero algo genérica. Útil como punto de partida.
2. Sistemático
Para esta función, genera edge cases en estas categorías:
empty, boundary, types, unicode, large inputs.
Incluye un ejemplo de input y el comportamiento esperado para cada uno.
Resultado típico: Lista organizada por categoría. Cubre el marco de las 10 categorías de forma explícita.
3. Adversarial
Intenta romper esta función con inputs inesperados.
Genera al menos 10 inputs que podrían causar errores o comportamiento indefinido.
Resultado típico: Enfocado en fallos. Útil para buscar bugs antes de que lleguen a producción.
4. Seguridad
¿Qué inputs maliciosos podrían causar comportamiento inesperado?
Piensa en injection, overflow, caracteres de control, etc.
Resultado típico: Casos de seguridad — path traversal, XSS, SQL injection, etc. Importante para APIs y procesamiento de datos de usuario.
Workflow práctico: de la función al suite de edge cases
Un flujo efectivo cuando trabajas con Claude Code:
- Primera pasada — Genérico: Pega la función y pregunta "¿Qué edge cases no estoy testeando?" para obtener una vista amplia.
- Segunda pasada — Sistemático: Pide explícitamente las categorías que faltaron (empty, boundary, unicode, etc.).
- Tercera pasada — Adversarial: "Intenta romper esta función" para casos de ataque o inputs malformados.
- Filtrado: Revisa la lista y descarta lo irrelevante (escenarios imposibles, impacto nulo).
- Implementación: Convierte los edge cases aprobados en tests parametrizados agrupados por categoría.
No necesitas hacer las cinco pasadas para cada función. Para código crítico (auth, pagos, parsers), haz las tres. Para utilidades simples, una o dos pasadas suelen bastar.
Ejemplo de output: prompt sistemático
Para la función parse_url:
Prompt:
Para parse_url(url: str) -> dict que retorna protocol, domain, path, params,
genera edge cases en: empty, boundary, types, unicode, large inputs.
Incluye input y comportamiento esperado.
Output típico de Claude Code:
| Categoría | Input | Comportamiento esperado |
|---|---|---|
| Empty | "" | Error claro o dict con valores vacíos |
| Empty | None | TypeError o validación explícita |
| Boundary | "a" | ¿Protocolo? ¿Dominio? |
| Boundary | "http://" | Dominio vacío |
| Types | 123 | TypeError |
| Unicode | "http://münchen.de/path" | Normalización/encoding |
| Unicode | "http://例え.jp/" | IDN handling |
| Large | string de 1M caracteres | Timeout, truncamiento, o límite razonable |
Con este tipo de prompts obtienes una matriz de casos que difícilmente generarías manualmente.
Workflow Práctico: De la Función al Suite de Edge Cases
Sigue este flujo cuando trabajes con Claude Code para edge case discovery:
Paso 1: Proporciona contexto completo
Comparte la firma de la función, su propósito y cualquier restricción conocida:
Tengo esta función que parsea URLs:
[pegar código]
El contrato es: acepta str no vacío, retorna dict con protocol, domain, path, params.
Debe rechazar "" y None con ValueError.
Paso 2: Pide edge cases por categorías
Usa el prompt sistemático con el marco de 10 categorías:
Genera edge cases en: empty, boundary, types, unicode, large inputs, format.
Para cada uno: input ejemplo, comportamiento esperado, y por qué importa.
Paso 3: Filtra lo que no aplica
Revisa la lista generada. Descarta:
- Casos fuera del contrato (ej. si la función no acepta
None, no testear "qué hace con None" si siempre lanzará) - Casos extremadamente improbables sin impacto
- Duplicados conceptuales
Paso 4: Pide tests parametrizados
Convierte estos edge cases en tests pytest parametrizados.
Agrupa por categoría (empty_null, boundary, unicode, etc.).
Usa pytest.param con ids descriptivos.
Paso 5: Ejecuta y ajusta
Ejecuta pytest -v, revisa qué pasa y qué falla. Ajusta expectativas si el comportamiento real difiere de lo que asumiste (tal vez la función maneja un caso que no conocías).
Ejemplo Real: Parser de URLs
La función
# url_parser.py
from urllib.parse import urlparse, parse_qs
from typing import Any
def parse_url(url: str) -> dict[str, Any]:
"""
Parse URL into components.
Returns: {"protocol": ..., "domain": ..., "path": ..., "params": ...}
"""
if not url or not isinstance(url, str):
raise ValueError("URL must be non-empty string")
parsed = urlparse(url)
params = {}
if parsed.query:
params = {k: v[0] if len(v) == 1 else v for k, v in parse_qs(parsed.query).items()}
return {
"protocol": parsed.scheme or None,
"domain": parsed.netloc or None,
"path": parsed.path or "/",
"params": params,
}
5 edge cases que un humano suele considerar
"https://example.com"— URL estándar"https://example.com/path"— Con path"https://example.com?key=value"— Con query string""— Vacío (tal vez)"http://example.com"— Sin SSL
15 edge cases que Claude Code puede generar
""— string vacíoNone— no es string" "— solo espacios"example.com"— sin protocolo"://example.com"— protocolo vacío"http://"— dominio vacío"http://example.com/"— path vacío (raíz)"http://example.com//double/slash"— doble slash en path"http://example.com/path?="— query con=sin key"http://münchen.de"— dominio con caracteres Unicode"http://例え.jp/path"— dominio IDN"http://example.com/path?key="— valor vacío en param"http://example.com?key=1&key=2"— mismo key repetido"http://" + "a" * 10_000_000— URL extremadamente larga"http://example.com/%00/path"— null byte en path
La brecha
Un humano suele cubrir el 1–5. Claude Code añade sistemáticamente 6–15: URLs sin protocolo, doble slash, Unicode, IDs duplicados en query, tamaños extremos, null bytes. Esos son los que suelen causar bugs en producción.
Evaluar Edge Cases Generados por IA
No todos los edge cases merecen un test. Pregúntate:
¿Es un escenario realista?
"http://example.com"→ sí"http://" + "x" * 10_000_000→ poco probable en uso normal, pero puede ser ataque
¿Un bug aquí tendría impacto real?
- Corrupción de datos → sí
- Error de seguridad → sí
- Mensaje de error feo en un caso rarísimo → quizá no
Criterios para incluir
- ✅ Seguridad: inyección, overflow, caracteres de control
- ✅ Corrupción de datos: inputs que podrían dañar el estado
- ✅ Errores visibles al usuario: mensajes confusos o crashes evidentes
Criterios para omitir
- ❌ Escenarios que requieren hardware o configuración muy específica
- ❌ Casos puramente teóricos sin aplicación práctica
- ⚠️ Casos extremadamente improbables con impacto mínimo
Construir una Suite de Edge Cases
Usar parametrize para agrupar por categoría
# test_url_parser.py
import pytest
from url_parser import parse_url
# Edge cases: empty/null
@pytest.mark.parametrize("url", ["", " ", None], ids=["empty", "whitespace", "none"])
def test_parse_url_rejects_empty_or_invalid(url):
if url is None:
with pytest.raises((TypeError, ValueError)):
parse_url(url)
else:
with pytest.raises(ValueError, match="non-empty"):
parse_url(url)
# Edge cases: boundary / format
@pytest.mark.parametrize("url,expected", [
("http://example.com", {"protocol": "http", "domain": "example.com", "path": "/", "params": {}}),
("http://example.com/", {"protocol": "http", "domain": "example.com", "path": "/", "params": {}}),
# "example.com" sin protocolo: urlparse pone el dominio en path, netloc vacío
("http://example.com/path", {"protocol": "http", "domain": "example.com", "path": "/path", "params": {}}),
], ids=["standard", "trailing_slash", "with_path"])
def test_parse_url_boundary_formats(url, expected):
result = parse_url(url)
assert result["protocol"] == expected["protocol"]
assert result["domain"] == expected["domain"]
assert result["path"] == expected["path"]
assert result["params"] == expected["params"]
# Edge cases: unicode (si tu parser los soporta)
@pytest.mark.parametrize("url", [
"http://münchen.de",
"http://例え.jp/",
], ids=["german_umlaut", "japanese_idn"])
def test_parse_url_unicode(url):
result = parse_url(url)
assert result["domain"] is not None
Documentar el por qué importa
@pytest.mark.parametrize("url,expected_path", [
# Double slashes: algunos sistemas tratan //path como absoluto
("http://example.com//foo", "//foo"),
# Query con key sin valor: APIs a veces envían key=
("http://example.com?a=", {"a": ""}),
], ids=["double_slash_path", "empty_param_value"])
def test_parse_url_format_edge_cases(url, expected_path):
result = parse_url(url)
# Documentar en comentario: "Double slash puede confundir routers"
assert "path" in result or "params" in result
Práctica: Suite Completa para un Validador
Función a testear:
# validators.py
def is_valid_username(username: str, min_len: int = 3, max_len: int = 20) -> bool:
"""
Validate username: alphanumeric + underscore, length between min_len and max_len.
"""
if not isinstance(username, str):
return False
if not username or not username.strip():
return False
cleaned = username.strip()
if not (min_len <= len(cleaned) <= max_len):
return False
return all(c.isalnum() or c == "_" for c in cleaned)
Suite de edge cases con parametrize:
# test_validators.py
import pytest
from validators import is_valid_username
@pytest.mark.parametrize("username,expected", [
("alice", True),
("bob", True),
("user_123", True),
("a" * 20, True),
("ab", False), # too short
("a" * 21, False), # too long
("", False),
(" ", False),
(" alice ", True), # strip
("alice!", False), # special char
("alice bob", False), # space
("Alice", True), # uppercase ok
("123", True), # numeric only
("_alone", True), # leading underscore
], ids=[
"valid_medium",
"valid_short",
"valid_with_underscore",
"valid_max_length",
"too_short",
"too_long",
"empty",
"whitespace_only",
"with_spaces",
"special_char",
"space_in_middle",
"uppercase",
"numeric_only",
"leading_underscore",
])
def test_is_valid_username(username, expected):
assert is_valid_username(username) == expected
@pytest.mark.parametrize("invalid_input", [None, 123, [], {}], ids=["none", "int", "list", "dict"])
def test_is_valid_username_rejects_non_string(invalid_input):
assert is_valid_username(invalid_input) is False
Ejecuta con pytest test_validators.py -v para ver cada caso por separado.
Ejercicios
Ejercicio 1: Categorizar edge cases
Tienes la función def count_words(text: str) -> int. Asigna cada caso a una categoría del marco de 10:
"""a" * 10_000_000"hello""hello\n\tworld"None"café"(con acento)" spaced "
Ver solución
| Input | Categoría |
|---|---|
"" | Empty/Null |
"a" * 10_000_000 | Very large inputs |
"hello" | Happy path (no es edge case) |
"hello\n\tworld" | Format edge cases (whitespace) |
None | Empty/Null / Type mismatches |
"café" | Unicode/Special chars |
" spaced " | Format edge cases (trailing/leading spaces) |
Ejercicio 2: Prompt para edge cases
Escribe un prompt que pida a Claude Code edge cases para def safe_divide(a: float, b: float) -> float que retorna a/b o 0 si b == 0.
Ver solución
Para safe_divide(a, b) que retorna a/b o 0 si b==0:
1. Genera edge cases en las categorías: empty/null, boundary, types.
2. Incluye: división por cero, numerador cero, negativos, floats grandes, tipos incorrectos.
3. Para cada caso indica input (a, b) y resultado esperado.
Alternativa más breve:
¿Qué edge cases debería testear en safe_divide(a, b)?
Cubre: b=0, a=0, negativos, tipos incorrectos, floats extremos.
Ejercicio 3: Tests parametrizados para edge cases
Implementa tests parametrizados para safe_divide que cubran: (10,2)->5, (0,5)->0, (5,0)->0, (-10,2)->-5, (10,-2)->-5, y rechazo de None.
Ver solución
# math_utils.py
def safe_divide(a: float, b: float) -> float:
if b == 0:
return 0.0
return a / b
# test_math_utils.py
import pytest
from math_utils import safe_divide
@pytest.mark.parametrize("a,b,expected", [
(10, 2, 5.0),
(0, 5, 0.0),
(5, 0, 0.0),
(-10, 2, -5.0),
(10, -2, -5.0),
], ids=["normal", "zero_numerator", "zero_denominator", "negative_a", "negative_b"])
def test_safe_divide(a, b, expected):
assert safe_divide(a, b) == expected
def test_safe_divide_rejects_none():
with pytest.raises(TypeError):
safe_divide(None, 5)
Si safe_divide no valida tipos y deja que falle con TypeError, el test puede usar pytest.raises(TypeError) como arriba.
Ejercicio 4: La brecha humano vs IA
Para def get_extension(filename: str) -> str que retorna la extensión (ej. "file.txt" → "txt"), lista 3 edge cases que suele considerar un humano y 5 que suele añadir Claude Code.
Ver solución
Humanos suelen pensar en:
"file.txt"→"txt""archive.tar.gz"→"gz"o"tar.gz"según diseño"no_extension"→""o el nombre completo
Claude Code suele añadir:
4. "" → ¿qué retornar?
5. ".hidden" → extensión vacía o nombre oculto
6. "file." → extensión vacía
7. "file.Ñ.txt" → Unicode en extensión
8. "C:\\path\\file.txt" o "/path/.hidden/file.txt" → paths con separadores
9. "file" + "\0" + ".txt" → null byte en el nombre
Ejercicio 5: Evaluar y filtrar
Claude Code sugiere estos edge cases para un API que procesa page y limit:
page=-1page=0page=2**64page="one"limit=0limit=-1page=1, limit=10(happy path)page=1, limit=10ejecutado 1000 veces en 1ms (concurrency)
Indica cuáles incluirías y cuáles descartarías, con razón.
Ver solución
| Caso | Incluir | Razón |
|---|---|---|
page=-1 | Sí | Paginación inválida, muy realista |
page=0 | Sí | Puede ser válido o no según diseño |
page=2**64 | Opcional | Overflow, más relevante si usas int de 32 bits |
page="one" | Sí | Error de tipo típico (query params como string) |
limit=0 | Sí | Puede generar errores o resultados vacíos |
limit=-1 | Sí | Inválido, puede usarse para extraer muchos datos |
| Happy path | Sí | Siempre incluir |
| Concurrency 1000x | No (o aparte) | Es test de carga, no unitario |
Ejercicio 6: Suite de edge cases con parametrize
La función def parse_list_from_string(s: str) -> list[str] espera strings como "a,b,c" y retorna ["a","b","c"]. Escribe una suite parametrizada que cubra: vacío, un solo elemento, espacios, duplicados, string muy largo, coma final, coma inicial.
Ver solución
# parsers.py
def parse_list_from_string(s: str) -> list[str]:
if not s or not isinstance(s, str):
return []
return [part.strip() for part in s.split(",") if part.strip()]
# test_parsers.py
import pytest
from parsers import parse_list_from_string
@pytest.mark.parametrize("s,expected", [
("", []),
("a", ["a"]),
("a,b,c", ["a", "b", "c"]),
(" a , b , c ", ["a", "b", "c"]),
("a,a,a", ["a", "a", "a"]),
("a,,b", ["a", "b"]),
("a,", ["a"]),
(",a", ["a"]),
], ids=[
"empty",
"single",
"multiple",
"with_spaces",
"duplicates",
"empty_parts",
"trailing_comma",
"leading_comma",
])
def test_parse_list_from_string(s, expected):
assert parse_list_from_string(s) == expected
def test_parse_list_from_string_rejects_none():
with pytest.raises((TypeError, AttributeError)):
parse_list_from_string(None)
Si parse_list_from_string acepta None y retorna [], el test para None sería un assert parse_list_from_string(None) == [].
Troubleshooting
1. Claude Code genera demasiados edge cases
Problema: La respuesta incluye 50+ casos y no sabes por dónde empezar.
Solución: Pide que priorice:
De los edge cases que generaste, prioriza los 10 más importantes.
Criterios: impacto en seguridad, corrupción de datos, errores visibles al usuario.
O restringe categorías:
Genera solo edge cases en las categorías: empty, boundary, types.
Máximo 3 por categoría.
2. Los tests parametrizados fallan por tipos
Problema: pytest.raises en un caso y assert en otro, mezclados en el mismo parametrize.
Solución: Separa tests: uno para el happy path y casos normales, otro para excepciones:
@pytest.mark.parametrize("input_val,expected", [(1, 1), (2, 2)])
def test_valid(input_val, expected):
assert func(input_val) == expected
@pytest.mark.parametrize("invalid", [None, ""])
def test_invalid(invalid):
with pytest.raises(ValueError):
func(invalid)
3. Edge cases que dependen de implementación
Problema: Claude Code propone casos que solo tendrían sentido con otra implementación.
Solución: Aclara el contrato:
Genera edge cases para esta función.
El contrato es: acepta str no vacío, retorna dict. No acepta None.
Enfócate en edge cases dentro de ese contrato.
4. Duplicación entre tests generados por IA
Problema: Varios tests cubren el mismo edge case con formulaciones distintas.
Solución: Pide consolidación:
Revisa estos tests. Consolidar casos duplicados usando @pytest.mark.parametrize.
Mantener un caso por comportamiento único.
5. Edge cases irreales o imposibles
Problema: Casos como "disco lleno" o "red caída" para una función pura.
Solución: Acota el dominio:
Esta función es pura: recibe string, retorna string.
No considera I/O, red ni sistema de archivos.
Genera edge cases solo sobre el input string.
Conexión con Proyecto
En el proyecto de este módulo ("Suite con 90%+ coverage") trabajarás con código existente que tiene gaps de coverage. Edge case discovery con Claude Code te sirve para:
- ✅ Cerrar gaps de coverage en ramas y condiciones poco ejercitadas
- ✅ Encontrar comportamientos inesperados antes de que lleguen a producción
- ✅ Documentar el por qué de cada edge case (seguridad, datos, UX)
Workflow sugerido:
- Medir coverage (
pytest --cov) - Identificar funciones con baja cobertura o ramas sin cubrir
- Pedir a Claude Code edge cases para esas funciones con prompts sistemáticos
- Filtrar los casos relevantes
- Implementar tests parametrizados agrupados por categoría
- Volver a medir coverage
La siguiente cápsula (05) complementa esto con boundary value analysis y property-based testing con hypothesis.
Próxima cápsula: Cápsula 05 — Boundary value analysis y property-based testing con hypothesis. Aprenderás a definir intervalos de equivalencia, valores límite justo dentro y fuera de los rangos, y cómo hypothesis puede generar miles de inputs automáticamente para descubrir casos que ni tú ni Claude Code habríais pensado.
Resumen
- ✅ Los humanos solemos enfocarnos en el happy path y en casos familiares; Claude Code puede explorar categorías de edge cases de forma más sistemática
- ✅ Usa el marco de 10 categorías: Empty/Null, Boundary, Types, Unicode, Large/Small inputs, Duplicates, Ordering, Concurrency, Format
- ✅ Prompts útiles: genérico, sistemático, adversarial, de seguridad
- ✅ No todos los edge cases merecen test: prioriza impacto en seguridad, datos y usuario
- ✅ Agrupa tests con
@pytest.mark.parametrizepor categoría y documenta el por qué importan - ✅ Usa edge case discovery para cerrar gaps de coverage en el proyecto del módulo
Recursos Adicionales
- OWASP Testing Guide — Edge cases y pruebas de seguridad
- Hypothesis: What is property-based testing? — Generación automática de inputs
- pytest: Parametrize — Parametrización de tests
- Boundary Value Analysis (Wikipedia) — Análisis de valores límite
- Python URL parsing (urllib.parse) — Referencia para el ejemplo de URLs
- Fuzzing with AFL — Enfoque adversarial automatizado para inspiración
Próxima cápsula: La cápsula 05 cubre boundary value analysis y property-based testing con hypothesis — el complemento que descubre inputs problemáticos de forma automática donde los prompts cubren lo conocido.
Módulo 5, Cápsula 04 — Testing with Claude Code Guide