Por qué los edge cases rompen tu código
El usuario promedio es predecible. El usuario real pega 'undefined' como texto en un campo, copia emojis compuestos que JavaScript cuenta como 4 caracteres, manda fechas del año 9999, o sube archivos llamados ../../etc/passwd. Si tus tests solo prueban el happy path, el código explota en producción.
Los edge cases no son raros: son inevitables a escala. Con 10 usuarios nunca ves null inesperado. Con 10,000 usuarios pasa 3 veces al día. Con 1M de usuarios, todo input inválido posible va a ocurrir. El problema no es la probabilidad, es la superficie de ataque. Cada input sin validar es un bug esperando a manifestarse.
Los bugs más caros vienen de asumir. Asumís que user.name siempre existe. Asumís que el ID es número positivo. Asumís que el string no tiene caracteres especiales. Un solo null no manejado en función crítica tira el sistema. Tests con edge cases convierten esas asunciones en verificaciones explícitas. No es paranoia, es experiencia acumulada de toda la industria.
Categorías de edge cases que importan
Nullish: null, undefined, NaN. JavaScript tiene 7+ maneras de representar 'nada'. == null cubre null y undefined, pero NaN necesita Number.isNaN(). Infinity y -Infinity son números válidos pero rompen aritmética normal. Testeá divisiones por cero, Math.log(0), Math.sqrt(-1).
Strings problemáticos: vacío, espacios, saltos de línea, caracteres Unicode raros (zero-width, control chars, emojis compuestos). Un emoji como 👨👩👧👦 es técnicamente 7 code points pero visualmente 1 caracter. string.length miente. [...string].length también miente con algunos casos. Usá librerías tipo grapheme-splitter para contar correctamente.
Números límite: Number.MAX_SAFE_INTEGER (2^53 - 1) es el último entero que JavaScript maneja sin pérdida de precisión. Más allá necesitás BigInt. 0.1 + 0.2 !== 0.3 es famoso pero sigue rompiendo cálculos financieros. Nunca compares floats con ===. Usá tolerancia: Math.abs(a - b) < Number.EPSILON. O mejor: trabajá con enteros (centavos en lugar de dólares).
Cómo estructurar tests de edge cases
No pongas 50 edge cases en un solo test. Si falla, no sabés cuál input lo rompió. Un test por edge case, con nombre descriptivo: test('should handle null user id'), test('should reject negative amounts'). Cuando falle, el nombre del test te dice exactamente qué está roto.
Usá test.each o describe.each para parametrizar: test.each([null, undefined, NaN])('handles %p gracefully', (value) => { ... }). Mantiene DRY sin sacrificar claridad. El output muestra qué valor específico falló. En Jest, %p pretty-printa el valor; en Vitest es igual.
Priorizá edge cases por riesgo. Inputs financieros (cantidades negativas, decimales raros) son críticos—pueden costar plata real. Inputs de autenticación (bypass con admin'--, SQL injection) son security. Inputs de UI (strings largos que rompen layout) son UX. Testeá críticos primero, cosméticos después. No todo edge case tiene igual peso.
Herramientas y estrategias avanzadas
Property-based testing con fast-check: en lugar de testear casos específicos, definís propiedades que deben cumplirse para cualquier input. Ejemplo: 'serializar y deserializar JSON debe retornar valor idéntico'. La librería genera cientos de inputs random (incluyendo edge cases) y verifica la propiedad. Encuentra bugs que nunca hubieras imaginado.
Fuzzing: herramientas como jsfuzz o jazzer.js generan inputs inválidos masivos y mutados para buscar crashes. Útil para parsers, deserializadores, validadores. Si tu código procesa input externo (APIs, uploads, formularios), el fuzzing encuentra vulnerabilidades que tests manuales no cubren.
Mutation testing con Stryker: modifica tu código (cambia > por >=, && por ||) y re-corre tests. Si los tests siguen pasando con código mutado, significa que no están cubriendo esos casos. Te fuerza a escribir mejores aserciones. Es lento pero expone huecos reales en cobertura. Correlo en CI una vez por semana, no en cada commit.