Número mágico en tests.
Un test codifica de forma rígida literales numéricos sin explicar en sus entradas y aserciones, ocultando qué significan los números y de dónde salieron.
##Signs and Symptoms
Detectas un Magic Number Test cuando los argumentos y las aserciones de un test están llenos de números sueltos cuyo significado y origen no son evidentes a partir del código. El lector tiene que aplicar ingeniería inversa (o simplemente confiar) para entender por qué se espera un valor concreto.
Señales reveladoras:
- Aparecen literales numéricos directamente como argumentos de aserción:
expect(result).toBe(54.13),assertEquals(86400, ttl). - El mismo literal se repite en la configuración, la acción y el valor esperado, sin ningún nombre que los relacione.
- Los números codifican conceptos de dominio que no se explicitan (
3600= una hora,200= HTTP OK,0.0825= un tipo impositivo). - Un comentario de código se coloca junto al número para explicarlo, señal de que el número en sí debería haber tenido un nombre.
- Durante la revisión la gente pregunta «¿por qué
42?» o «¿de dónde sale54.13?» y nadie puede responder sin volver a ejecutar el código.
// Mal olor: ¿qué es 8.25? ¿por qué 54.13? ¿qué es ese 50 oculto dentro del helper?
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
Esto es una variante específica, del lado de los tests, del mal olor general Magic Number, y un contribuyente clásico al mal olor Obscure Test (Meszaros): el lector no puede entender el test solo a partir del test.
##Reasons for the Problem
Por qué ocurre
- El literal es el camino de menor resistencia: escribes el valor que viste en un depurador o copias la salida real de una ejecución fallida en la aserción hasta que pasa a verde («adivina el valor» / pegado de salidas).
- El autor ya tiene en la cabeza el contexto del dominio, así que
3600u8.25le parecen evidentes en el momento de escribirlo. - Los valores del fixture se eligen de forma arbitraria (
new User(25, ...)) solo para satisfacer al constructor, sin pensar en su significado.
Por qué es perjudicial
- Legibilidad / intención. Un número como
54.13expresa un hecho, pero no una razón. Los revisores y los futuros responsables del mantenimiento no pueden saber si es una expectativa deliberada, un límite o un accidente. El test deja de ser documentación ejecutable. - Mantenibilidad. Cuando la regla cambia (el tipo impositivo, el timeout, el tamaño de página), tienes que rastrear cada copia del literal y saber qué
7significaba «días» frente a «reintentos máximos». La duplicación sin nombre hace que las modificaciones seguras sean caras y propensas a errores. - Fiabilidad / falsa confianza. Si el valor esperado es incorrecto —o correcto solo por casualidad—, nada en el test lo revela. Peor aún, a menudo se «arregla» un test con números mágicos recalculando el valor esperado con la fórmula de producción (
expect(total).toBe(subtotal * (1 + rate)))), convirtiendo la aserción en una tautología que reimplementa el código bajo prueba y nunca puede fallar por la razón correcta. - Diagnóstico. Cuando un test así se rompe, el mensaje de fallo es simplemente «expected 54.13, got 54.12», sin pista alguna sobre qué entrada o regla produjo el número, lo que ralentiza la depuración.
##Treatment
Aplica Replace Magic Number with Symbolic Constant (Meszaros): dale a cada valor significativo un nombre que explique su papel, y haz explícita la relación entre las entradas y el resultado esperado.
Pasos concretos:
- Nombra las entradas. Extrae los literales usados como datos de test a constantes locales bien nombradas o a llamadas a un constructor de fixtures (
const SUBTOTAL = 50.00,const TAX_RATE_PCT = 8.25). Usa un Object Mother / builder para los fixtures de objetos, de modo que solo sean visibles los valores que importan al test. - Nombra y explica el valor esperado. Mantén el resultado esperado como un literal independiente, pero nómbralo y documenta cómo se obtuvo (
const EXPECTED_TOTAL = 54.13; // 50.00 + 8.25% de impuesto). No lo recalcules con la fórmula de producción: eso solo vuelve a probar el código contra sí mismo. - Relaciona las entradas con la aserción para que un lector pueda verificar la aritmética a simple vista, o haz la aserción contra un valor de referencia derivado pero independiente.
- Deja en paz los valores realmente evidentes por sí mismos.
0,1,-1, los índices de array y los conteos obvios (items).toHaveLength(2)) normalmente no necesitan nombres; reserva las constantes para los valores cuyo significado no se explica por sí solo. Promueve una constante compartida a una única fuente de verdad solo cuando se trate verdaderamente del mismo concepto en todas partes.
// Antes
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
// Después
const SUBTOTAL = 50.00;
const TAX_RATE_PCT = 8.25;
const EXPECTED_TOTAL = 54.13; // SUBTOTAL más un 8.25% de impuesto sobre ventas
test('applies sales tax to the subtotal', () => {
const total = checkout(cartFor(SUBTOTAL), TAX_RATE_PCT);
expect(total).toBe(EXPECTED_TOTAL);
});
Ahora los nombres llevan la intención; si la regla del impuesto cambia, la modificación es local y evidente, y el valor esperado sigue siendo una comprobación honesta e independiente en lugar de una tautología.
##Detected by
- eslint no-magic-numbers — Los números mágicos deben declararse como constantes con nombre
- typescript-eslint @typescript-eslint/no-magic-numbers — Prohibir números mágicos (TypeScript)
- sonar javascript:S109 — No deben usarse números mágicos
- checkstyle MagicNumber — MagicNumber
- tsDetect Magic Number Test — Magic Number Test (detector específico de tests)