La métrica CRAP: complejidad y cobertura en un número
CRAP significa Change Risk Anti-Patterns. La métrica multiplica lo enredado de un método por lo poco probado que está, y el resultado responde a una pregunta que ninguna métrica suelta puede responder: ¿qué código es peligroso de cambiar?
Un instrumento deliberadamente tosco
Alberto Savoia y Bob Evans presentaron CRAP en 2007 junto con Crap4j, una herramienta Java que puntuaba cada método de una compilación. La premisa era que ni la complejidad ni la cobertura significan mucho por separado. Un método enrevesado con pruebas sólidas es manejable. Un método trivial sin pruebas no molesta. Lo que de verdad duele es la complejidad que nadie ha cubierto, porque ahí una edición pequeña produce una sorpresa que nadie atrapa. CRAP pone ambos términos en una sola expresión para que la combinación peligrosa puntúe mal y las inofensivas no.
El nombre hace un trabajo deliberado. De una métrica llamada Change Risk Anti-Patterns se habla una vez; una métrica que te dice que un método es una porquería se arregla.
La fórmula y por qué los exponentes son desiguales
La complejidad va al cuadrado. La fracción no cubierta va al cubo. Esa asimetría es todo el diseño.
- • CC es la complejidad ciclomática del método: el número de caminos independientes que lo atraviesan
- • cov es la cobertura del método como fracción de 0 a 1, así que (1 − cov) es la parte sin probar
- • Elevar al cubo la parte sin probar hace que el primer término se derrumbe rápido al subir la cobertura, de modo que probar un método feo paga de inmediato
- • Con cobertura completa el primer término es cero y CRAP equivale a CC, el riesgo residual que las pruebas no pueden quitar
- • El + CC final impide que la métrica finja alguna vez que la complejidad es gratis
Lo que los números exigen de verdad
Con el umbral convencional de 30, esta es la cobertura que necesita cada nivel de complejidad para bajar de la línea.
| CC | 0% cov | 50% cov | 100% cov | To clear 30 |
|---|---|---|---|---|
| 5 | 30.0 | 8.1 | 5 | Ninguna. El código simple pasa sin pruebas. |
| 10 | 110.0 | 22.5 | 10 | Alrededor del 42 % |
| 15 | 240.0 | 43.1 | 15 | Alrededor del 60 % |
| 20 | 410.0 | 70.0 | 20 | Alrededor del 71 % |
| 25 | 650.0 | 103.1 | 25 | Exactamente el 80 % |
| 30 | 930.0 | 142.5 | 30 | 100 %, y aterriza justo en la línea |
| 31+ | 961.0 | 155.2 | 31 | Inalcanzable. Las pruebas no arreglan este caso. |
Qué te está diciendo la curva
Pasada la complejidad 30, la cobertura deja de ayudar
Con un umbral de 30, un método de complejidad 31 queda por encima de la línea incluso al 100 % de cobertura. No es un defecto de la fórmula, es su mensaje: el único movimiento que queda es partir el método.
Con cobertura completa, CRAP es solo complejidad
Las pruebas nunca perdonan la complejidad, solo dejan de amplificarla. Un método complejo bien cubierto sigue cargando su complejidad como riesgo reconocido y gestionado en lugar de riesgo oculto.
El código simple se deja en paz a propósito
Un método de complejidad 5 está exactamente en 30 sin ninguna prueba. La métrica dirige la atención al código enredado en vez de generar trabajo inútil sobre getters y mappers.
La cobertura es la pata débil
cov mide ejecución, no aserción. Una suite sin aserciones sube la cobertura y baja el CRAP sin cambiar nada del riesgo real. Ese hueco concreto es el que existen para cerrar las pruebas de mutación.
Trabajar con ella
La fórmula son dos líneas de código, y eso explica en buena parte por qué se reimplementa una y otra vez en ecosistemas nuevos.
La métrica en sí
jsCualquier informe de cobertura más cualquier herramienta de complejidad da todo lo que la fórmula necesita.
// crap.js — coverage is a fraction, 0..1
export function crap(complexity, coverage) {
const untested = 1 - coverage;
return complexity ** 2 * untested ** 3 + complexity;
}
crap(15, 0); // 240
crap(15, 0.5); // 43.125
crap(15, 1); // 15 Poner barrera al cambio, no a la base de código
shUn trinquete sobre los métodos cambiados mantiene fuera el riesgo nuevo sin abrir un proyecto de limpieza que nadie financió. Las puntuaciones heredadas siguen visibles en un panel en lugar de bloquear cada compilación.
# CI: score only the methods this branch touched
git diff --name-only origin/main... -- '*.js' \
| xargs node ./tools/crap-report.mjs --threshold 30 --changed-only
# exit non-zero when a touched method crosses the line;
# print the untouched offenders as a report, not a failure El refactor que la puntuación está pidiendo
jsCuando la puntuación viene de la complejidad y no de la cobertura, probar más fuerte es la respuesta equivocada. Saca cada rama a algo de complejidad 1 y el conductor se queda plano por muchas reglas que lleguen.
// Before: complexity climbs with every rule anyone adds.
function validate(order) {
if (!order.id) return 'missing id';
if (order.items.length === 0) return 'no items';
if (order.total < 0) return 'negative total';
if (order.currency !== 'USD' && order.currency !== 'EUR') return 'bad currency';
if (order.customer && !order.customer.email) return 'customer without email';
return null;
}
// After: each rule is trivially testable, the driver stays at 2.
const RULES = [
[(o) => !o.id, 'missing id'],
[(o) => o.items.length === 0, 'no items'],
[(o) => o.total < 0, 'negative total'],
[(o) => !['USD', 'EUR'].includes(o.currency), 'bad currency'],
[(o) => Boolean(o.customer) && !o.customer.email, 'customer without email']
];
function validate(order) {
for (const [fails, message] of RULES) if (fails(order)) return message;
return null;
} Cómo usarla sin irritar a todo el mundo
Leer las dos palancas por separado
- - Una puntuación alta empujada por la complejidad es tarea de refactor, no de pruebas
- - Una puntuación alta empujada por la falta de cobertura es tarea de pruebas, y barata
- - Muestra siempre CC y cobertura junto a la puntuación, porque el número solo no dice cuál de las dos es
Ponderar por frecuencia de cambio
- - El riesgo de cambio solo importa donde ocurre el cambio
- - Una puntuación de 200 en un archivo intacto desde hace cuatro años es menos urgente que un 60 en uno editado cada semana
- - Ordena por puntuación multiplicada por frecuencia de commits y trabaja desde arriba de esa lista
Hacer explícitas las exclusiones
- - El código generado, los adaptadores y los switch exhaustivos inflan la complejidad sin inflar el riesgo real
- - Exclúyelos en configuración, con un comentario que diga por qué, en lugar de subir el umbral en silencio
- - Revisa la lista de exclusiones cuando el código que protege deje de estar generado
Malas lecturas habituales
Tratarla como nota de calidad
CRAP estima el riesgo de cambiar código. No dice nada de si el código es correcto, está bien nombrado o bien diseñado. Un método limpio y bien cubierto con complejidad de dominio genuina puntúa igual que uno desagradable.
Trucarla con cobertura
Las pruebas que capturan todo en snapshot y los recorridos sin aserciones mueven el número, no el riesgo. Si CRAP es una barrera, algo tiene que mantener honestas las pruebas: la revisión, las pruebas de mutación, o ambas.
Lanzar una épica de limpieza
Un informe CRAP de todo el repositorio sobre código heredado produce un número tan grande que se ignora. Pon el trinquete sobre el código cambiado y deja que las partes peligrosas las arregle quien ya iba a tocarlas.
Esperar una herramienta mantenida
El Crap4j original lleva años dormido. NDepend lleva la métrica en .NET y hay implementaciones comunitarias para Rust, .NET y Groovy, pero en la mayoría de los stacks la calculas tú mismo con datos que ya recoges.
Un número, una pregunta
CRAP no es una nota de calidad ni pretendió serlo. Responde a una pregunta más estrecha y más útil: si alguien edita este método la semana que viene, ¿qué probabilidad hay de que algo se rompa en silencio? La complejidad dice de cuántas formas se puede equivocar uno, la cobertura dice cuántas de ellas está vigilando alguien, y la fórmula pesa más lo segundo que lo primero.
Con eso ya merece estar en la compilación. Pon barrera al código nuevo y cambiado en un umbral, ordena el resto por puntuación frente a frecuencia de cambio y lee las dos entradas por separado para que el número se traduzca en una acción: partir este método o probarlo. Solo recuerda que la mitad de cobertura de la fórmula significa únicamente lo que tus aserciones le hagan significar.
Artículos de ingeniería relacionados
La mitad de cobertura de esta fórmula es justo lo que interrogan las pruebas de mutación, y ya tiene un rol propio en los flujos con agentes.
Las pruebas de mutación son negativas. Las de TDD, positivas.
Una ejecución de mutación solo informa de lo que una suite no detecta, mientras una prueba de TDD afirma lo que el sistema debe hacer. Cómo usar cada una en consecuencia.
SwarmForge: lo que acierta el enjambre de agentes de Uncle Bob
Una revisión de la descomposición en roles, el aislamiento por worktree y el protocolo de traspaso de SwarmForge, y de qué partes sirven en cualquier equipo.
Preguntas frecuentes
¿Qué se considera una mala puntuación CRAP?
Treinta es el umbral por defecto convencional, heredado de Crap4j y reutilizado por casi todas las implementaciones posteriores. Bájalo en código nuevo, donde mantener la línea cuesta poco, y consérvalo en código heredado mientras aprietas el trinquete en lugar de subirlo para que un informe se vea mejor.
¿Por qué la complejidad al cuadrado y la parte sin probar al cubo?
Para que la cobertura mueva la puntuación más rápido que la complejidad. El término al cubo se desploma hacia cero cuando la cobertura se acerca al total, lo que premia de inmediato probar código complejo, mientras el término al cuadrado más la complejidad final dejan un suelo que ninguna prueba puede quitar.
¿Sigue siendo relevante la métrica CRAP?
La herramienta Java original lleva mucho tiempo dormida, pero la fórmula reaparece en ecosistemas nuevos porque es trivial de implementar y responde a una pregunta que los paneles de una sola métrica no pueden. Donde ya recojas cobertura y complejidad, estás a dos líneas de tenerla.
¿Una puntuación CRAP baja significa que el código es seguro de cambiar?
Significa que la complejidad está cubierta, que no es lo mismo que bien probada. La cobertura cuenta ejecución en lugar de aserción, así que una puntuación baja sostenida por pruebas débiles es un número cómodo sobre una realidad incómoda. Las pruebas de mutación son la forma habitual de comprobar si la cobertura es real.
¿Debería CRAP romper la compilación?
Como trinquete sobre métodos nuevos y cambiados, sí, porque es aplicable y nadie tiene que planificarlo. Como barrera de todo el repositorio sobre una base existente, no: eso produce un backlog en lugar de un cambio de comportamiento.