La métrique CRAP : trouver la complexité non testée
CRAP signifie Change Risk Anti-Patterns. La métrique multiplie l'enchevêtrement d'une méthode par son défaut de tests, et elle répond à une question qu'aucune métrique seule ne traite : quel code est dangereux à modifier ? Cette question est devenue beaucoup plus urgente depuis que des agents écrivent le code.
Un instrument volontairement grossier
Alberto Savoia et Bob Evans ont introduit CRAP en 2007 avec Crap4j, un outil Java qui notait chaque méthode d'un build. Le postulat : ni la complexité ni la couverture ne signifient grand-chose seules. Une méthode tortueuse bien testée est vivable. Une méthode triviale sans tests ne pose pas de problème. Ce qui fait vraiment mal, c'est la complexité que personne n'a couverte, car c'est là qu'une petite modification produit une surprise que personne n'attrape. CRAP met les deux termes dans une seule expression, pour que la combinaison dangereuse soit mal notée et les combinaisons inoffensives non.
Le nom fait un travail délibéré. On discute une fois d'une métrique appelée Change Risk Anti-Patterns ; une métrique qui vous dit qu'une méthode est nulle, on la corrige.
À quoi elle sert réellement
CRAP n'est pas un ornement de tableau de bord. Chaque score se résout en l'une de quatre décisions, et c'est toute la raison de le calculer.
Où va le prochain test
Trié par ordre décroissant, le rapport est une file de travail pour l'effort de test. Le haut de la liste est là où un test achète le plus de réduction de risque par heure passée, ce qui vaut bien mieux que courir après un pourcentage global de couverture.
Quoi refactorer avant d'y toucher
Un score élevé tiré par la complexité prévient que la méthode va vous résister. La découper avant d'ajouter du comportement coûte généralement moins cher que d'ajouter le comportement puis d'essayer de tester le résultat.
Quel changement mérite une lecture humaine attentive
Un diff qui augmente le CRAP des méthodes qu'il touche mérite une revue ligne par ligne. Un diff qui le baisse peut être survolé. Cette décision d'orientation est là où la métrique se rembourse en temps de revue.
Quoi tester en régression avant une release
Le risque de changement combiné à la fréquence récente de modification vous dit quels modules ont mérité une passe de régression cette release, et lesquels vous pouvez croire parce que rien n'y a bougé.
La formule, et pourquoi les exposants sont inégaux
La complexité est au carré. La fraction non couverte est au cube. Cette asymétrie est tout le principe.
- • CC est la complexité cyclomatique de la méthode : le nombre de chemins indépendants qui la traversent
- • cov est la couverture de la méthode, de 0 à 1, donc (1 − cov) est la part non testée
- • Mettre la part non testée au cube fait s'effondrer vite le premier terme quand la couverture monte : tester une méthode laide paie immédiatement
- • À couverture complète le premier terme est nul et CRAP vaut CC, le risque résiduel que les tests ne peuvent pas retirer
- • Le + CC final empêche la métrique de prétendre un jour que la complexité est gratuite
Ce que les chiffres exigent réellement
Avec le seuil conventionnel de 30, voici la couverture nécessaire à chaque niveau de complexité pour passer sous la ligne.
| CC | 0% cov | 50% cov | 100% cov | To clear 30 |
|---|---|---|---|---|
| 5 | 30.0 | 8.1 | 5 | Aucune. Le code simple passe sans tests. |
| 10 | 110.0 | 22.5 | 10 | Environ 42 % |
| 15 | 240.0 | 43.1 | 15 | Environ 60 % |
| 20 | 410.0 | 70.0 | 20 | Environ 71 % |
| 25 | 650.0 | 103.1 | 25 | Exactement 80 % |
| 30 | 930.0 | 142.5 | 30 | 100 %, et cela tombe pile sur la ligne |
| 31+ | 961.0 | 155.2 | 31 | Inatteignable. Les tests ne sauvent pas celle-là. |
Ce que la courbe vous dit
Au-delà d'une complexité de 30, la couverture n'aide plus
Avec un seuil de 30, une méthode de complexité 31 est au-dessus de la ligne même à 100 % de couverture. Ce n'est pas un défaut de la formule, c'est son message : le seul geste restant est de découper la méthode.
À couverture complète, CRAP n'est que la complexité
Les tests ne pardonnent jamais la complexité, ils cessent seulement de l'amplifier. Une méthode complexe bien couverte porte sa complexité comme un risque reconnu et géré, plutôt que caché.
Le code simple est épargné exprès
Une méthode de complexité 5 est pile à 30 sans le moindre test. La métrique dirige l'attention vers le code enchevêtré au lieu de générer du travail inutile sur des getters et des mappers.
La couverture est la jambe faible
cov mesure l'exécution, pas l'assertion. Une suite sans assertions augmente la couverture et baisse le CRAP sans rien changer au risque réel. C'est précisément cette faille que les tests de mutation existent pour combler, et celle dans laquelle un LLM tombe tout droit.
Comment une équipe QA l'utilise
C'est la partie généralement absente des articles sur les métriques. CRAP est un instrument de visée, et la QA est celle qui le pointe.
Viser la suite de régression au lieu de la faire grossir
- - Classez les méthodes par CRAP multiplié par la fréquence des commits, et construisez la passe de régression de release depuis le haut de cette liste
- - Les modules à faible CRAP et sans modification n'ont pas besoin d'une passe à chaque release, et c'est de là que vient le temps pour le haut de la liste
- - Reclassez à chaque release plutôt que d'entretenir une suite statique qui grossit sans fin et n'est jamais taillée
Décider entre automatisation et exploration
- - Complexité élevée et couverture faible est un manque de tests unitaires, pas un manque d'exploration — demander à la QA manuelle de couvrir 20 branches à la main gâche un bon testeur
- - Complexité élevée et couverture élevée est là où les tests exploratoires paient, parce que les chemins sont exercés mais les exigences peuvent rester fausses
- - Le comportement indéfini se cache dans les branches non couvertes des méthodes complexes : c'est là qu'on conçoit d'abord les cas négatifs et limites
Rendre les barrières de release discutables en chiffres, pas en opinions
- - Une barrière du type aucune méthode modifiée au-dessus du seuil est applicable, relisable et difficile à contester en fin de sprint
- - Affichez les deux entrées à côté du score pour que le remède soit sans ambiguïté : découper, ou tester
- - Suivez les défauts échappés en fonction du CRAP de la méthode d'où ils venaient — cette corrélation est la façon d'ajuster votre propre seuil au lieu d'hériter du nôtre
En faire le langage commun avec le développement
- - Une objection qualité formulée comme cette méthode est brouillonne perd face aux délais ; formulée comme cette méthode est en complexité 24 avec 30 % de couverture, généralement non
- - Cela donne à la QA une raison légitime et préalablement admise de demander un refactoring avant l'arrivée d'une fonctionnalité
- - Cela protège aussi les développeurs du travail inutile, puisque la métrique dit explicitement que le code trivial n'a pas besoin de tests
Utiliser CRAP pour juger du code écrit par un LLM
Les agents ont changé l'économie de cette métrique. Ils produisent du code plausible plus vite que quiconque ne peut le lire, et ils écriront volontiers les tests qui les mesurent. Complexité contre couverture est l'un des signaux honnêtes les moins chers qui vous restent.
Le danger précis est qu'un LLM optimise l'objectif visible. Demandez des tests, vous obtenez des tests ; demandez de la couverture, vous obtenez de la couverture. Les deux moitiés de CRAP sont des choses qu'un agent peut déplacer, mais il ne peut en déplacer qu'une honnêtement. La complexité est une propriété structurelle du code qu'il a écrit — elle ne se négocie pas. La couverture est un nombre que l'agent peut gonfler en exécutant des lignes sans rien affirmer à leur sujet. Le couple est donc diagnostique : la complexité dit ce que l'agent a construit, la couverture dit ce qu'il en prétend, et l'écart entre les deux est l'endroit où regarder.
Noter le diff, pas le dépôt
- - Calculez le CRAP sur les méthodes touchées par l'agent, avant et après, et publiez le delta sur la pull request
- - Complexité en hausse avec couverture plate est la signature d'un comportement boulonné sur une fonction existante, la manière par défaut dont un agent ajoute une fonctionnalité
- - Complexité en baisse avec couverture en hausse, c'est à quoi ressemble un bon run d'agent, et cela mérite d'être récompensé par une revue plus rapide
Mettre un plafond de complexité dans les instructions de l'agent
- - L'essaim d'Uncle Bob fait exactement cela : le rôle cleaner lance d'abord l'outil CRAP et ramène la complexité à 6 ou moins avant toute autre chose
- - C'est un usage plus intelligent de la métrique que de barrer sur le score, car à complexité 6 le nombre ne peut presque plus monter, quoi que fasse la couverture
- - Donnez à l'agent le seuil et la commande, pas un paragraphe sur le clean code, et il s'y conformera parce que la vérification a un code de sortie
Renvoyer la sortie de l'outil dans la boucle
- - Un rapport CRAP dans un tableau de bord ne change rien ; le même rapport injecté dans le tour suivant de l'agent comme vérification en échec est corrigé
- - Lancez-le dans une étape séparée de celle qui a écrit le code, pour que l'agent ne note pas sa propre copie
- - Limitez les reprises — un agent qui n'arrive pas sous le seuil en deux tentatives vous dit que le design est mauvais, pas qu'il lui faut un essai de plus
Ne jamais laisser un agent à la fois augmenter la couverture et rapporter la métrique
- - La couverture est la moitié truquable, et un agent chargé d'améliorer son propre score trouvera le chemin le moins cher vers le nombre
- - Associez chaque barrière CRAP à un run de mutation, qui demande si ces nouveaux tests affirment quoi que ce soit
- - Traitez l'explication du score par l'agent comme de la publicité ; la preuve est la sortie de l'outil
Lire le signal sur du code écrit par un agent
Les mêmes chiffres veulent dire des choses précises et reconnaissables quand l'auteur est un modèle.
| Signal | Ce que cela signifie en général | Que faire |
|---|---|---|
| Complexité en hausse, couverture plate | Le nouveau comportement a été ajouté comme branches supplémentaires dans une fonction existante. | Demandez une extraction avant la revue. C'est la forme la plus courante de croissance du code d'agent. |
| Couverture en bond, complexité inchangée | Des tests ont été ajoutés. Qu'ils affirment quelque chose n'est pas encore établi. | Lancez les tests de mutation sur les fichiers modifiés avant de croire le chiffre. |
| Une méthode très au-dessus du seuil | Le modèle a continué d'empiler des cas au premier endroit trouvé, prompt après prompt. | Découpez par règle, puis relancez. Le score s'effondre généralement sans aucun test nouveau. |
| De la complexité dans du code que personne n'a demandé | Branches spéculatives, chemins défensifs et options sans exigence derrière. | Supprimez. De la complexité non testée qu'aucune exigence ne nomme est la chose la moins chère à retirer. |
| Score correct, couverture issue de tests snapshot | La suite exécute tout et ne vérifie presque rien. | La métrique vous mente à travers son entrée de couverture. Réparez les tests, pas le score. |
Travailler avec
La formule tient en deux lignes de code, ce qui explique en grande partie pourquoi elle est sans cesse réimplémentée dans de nouveaux écosystèmes.
La métrique elle-même
jsN'importe quel rapport de couverture plus n'importe quel outil de complexité fournit tout ce dont la formule a besoin.
// 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 Barrer le changement, pas la base de code
shUn cliquet sur les méthodes modifiées empêche l'arrivée de nouveau risque sans ouvrir un chantier de nettoyage que personne n'a financé. Les scores hérités restent visibles sur un tableau de bord au lieu de bloquer chaque build. Pour les branches écrites par un agent, c'est toute la barrière.
# 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 Le refactoring que le score réclame
jsQuand le score vient de la complexité et non de la couverture, tester plus fort est la mauvaise réponse. Sortez chaque branche dans quelque chose de complexité 1 et le pilote reste plat, quel que soit le nombre de règles à venir — y compris celles qu'un futur agent ajoutera.
// 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;
} La barrière qu'un agent ne peut pas contourner par la parole
shDeux vérifications, dans cet ordre, lancées par une étape qui n'a pas écrit le code. La première dit que le code a la forme de quelque chose que l'on peut changer ; la seconde dit que les tests qui le protègent réagissent vraiment quand il change.
# 1. structural: is this changeable code?
crap-report --changed-only --threshold 30 || exit 1
# 2. behavioural: do the new tests assert anything?
stryker run --incremental --mutate "$(git diff --name-only origin/main...)"
# report both on the PR. one number is about the code,
# the other is about the tests that claim to cover it. Comment l'utiliser sans agacer tout le monde
Lire les deux leviers séparément
- - Un score élevé tiré par la complexité est une tâche de refactoring, pas de test
- - Un score élevé tiré par la couverture manquante est une tâche de test, et peu coûteuse
- - Affichez toujours CC et la couverture à côté du score, car le chiffre seul ne dit pas lequel des deux
Pondérer par la fréquence de modification
- - Le risque de changement ne compte que là où le changement se produit
- - Un score de 200 dans un fichier intouché depuis quatre ans est moins urgent qu'un 60 dans un fichier modifié chaque semaine
- - Triez par score multiplié par la fréquence des commits et attaquez le haut de cette liste
Rendre les exclusions explicites
- - Le code généré, les adaptateurs et les switch exhaustifs gonflent la complexité sans gonfler le risque réel
- - Excluez-les en configuration, avec un commentaire expliquant pourquoi, plutôt que de relever discrètement le seuil
- - Revoyez la liste d'exclusions quand le code qu'elle protège cesse d'être généré
Mésinterprétations courantes
Le prendre pour une note de qualité
CRAP estime le risque de modifier du code. Il ne dit rien de sa justesse, de son nommage ou de son design. Une méthode propre et bien couverte, porteuse d'une vraie complexité métier, obtient le même score qu'une méthode désagréable.
Le truquer par la couverture
Les tests qui capturent tout en snapshot et les parcours sans assertions déplacent le chiffre, pas le risque. Si CRAP est une barrière, il faut autre chose pour garder les tests honnêtes : la revue, les tests de mutation, ou les deux. Avec un agent dans la boucle, supposez que cela arrivera sauf si une vérification séparée l'empêche.
Lancer un grand nettoyage
Un rapport CRAP à l'échelle du dépôt sur du code hérité produit un chiffre si gros qu'il est ignoré. Mettez plutôt un cliquet sur le code modifié et laissez les parties dangereuses être corrigées par ceux, humains ou agents, qui allaient déjà y toucher.
Attendre un outil maintenu
Le Crap4j original est en sommeil depuis des années. NDepend porte la métrique sur .NET, il existe des implémentations communautaires pour Rust, .NET et Groovy, et Uncle Bob maintient crap4j, crap4go et crap4clj pour son essaim d'agents. Sur la plupart des stacks vous la calculez vous-même à partir de données déjà collectées.
Un chiffre, une question
CRAP n'est pas une note de qualité et n'a jamais prétendu l'être. Il répond à une question plus étroite et plus utile : si quelqu'un modifie cette méthode la semaine prochaine, quelle est la probabilité que quelque chose casse silencieusement ? La complexité dit combien de façons il y a de se tromper, la couverture dit combien d'entre elles sont surveillées, et la formule pèse la seconde plus lourd que la première.
Cela en fait un outil de visée pour la QA et un outil d'audit pour le travail assisté par IA. Pointez-le sur le diff, barrez le code nouveau et modifié à un seuil, classez le reste par score contre fréquence de modification, et gardez les deux entrées visibles pour que le chiffre se traduise en action : découper cette méthode, ou la tester. Rappelez-vous seulement quelle moitié un auteur peut falsifier. La complexité est ce que le code est ; la couverture n'est que ce que les tests prétendent, et les tests de mutation sont la façon de vérifier la prétention.
Articles d'ingénierie liés
La moitié couverture de cette formule est exactement ce que les tests de mutation interrogent, et les deux métriques ont déjà leurs propres rôles dans un essaim d'agents en fonctionnement.
Les tests de mutation sont négatifs. Les tests TDD sont positifs.
Pourquoi un run de mutation ne signale que ce qu'une suite manque, comment le lancer à coût maîtrisé, et comment juger des tests qu'un LLM a écrits pour son propre code.
SwarmForge décrypté : comment l'essaim d'Uncle Bob fonctionne vraiment
Une analyse approfondie du pipeline de rôles, du démon de passation et des barrières qualité exécutables de SwarmForge, avec un verdict explicite sur ce qu'il faut adopter.
FAQ
Qu'est-ce qu'un mauvais score CRAP ?
Trente est le seuil par défaut conventionnel, héritée de Crap4j et reprise par la plupart des implémentations ultérieures. Abaissez-le sur du code neuf où tenir la ligne coûte peu, et gardez-le sur du code hérité pendant que vous serrez le cliquet, plutôt que de le relever pour embellir un rapport.
CRAP est-il utile pour relire du code généré par IA ?
C'est l'un des signaux peu coûteux les plus utiles, parce que la complexité est un fait structurel sur le code que le modèle a produit et ne se discute pas. Notez le diff plutôt que le dépôt, guettez la complexité qui monte quand la couverture reste plate, et associez-le toujours à un run de mutation pour que la moitié couverture ne soit pas gonflée par des tests qui n'affirment rien.
Quel seuil fixer pour des branches écrites par un agent ?
Plus serré que pour des humains, et exprimé en complexité plutôt qu'en score. Le schéma pratique, celui de l'essaim d'Uncle Bob, est un plafond dur de complexité autour de 6 sur les méthodes modifiées : à ce niveau le score CRAP ne peut plus beaucoup grimper quoi qu'il arrive à la couverture, donc il n'y a plus rien à négocier.
Un agent peut-il corriger son propre score CRAP ?
Généralement oui pour la moitié complexité, ce qui est une vraie amélioration et mérite d'être automatisé. Pour la moitié couverture, prudence : le moyen le moins cher d'augmenter la couverture est d'exécuter du code sans le vérifier. Lancez la barrière dans une étape qui n'a pas écrit le code, et validez les nouveaux tests par des tests de mutation.
Pourquoi la complexité au carré mais la part non testée au cube ?
Pour que la couverture déplace le score plus vite que la complexité. Le terme au cube s'effondre vers zéro quand la couverture approche du complet, ce qui récompense immédiatement le test du code complexe, tandis que le terme au carré plus la complexité finale laissent un plancher qu'aucun test ne retire.
CRAP doit-il faire échouer le build ?
En cliquet sur les méthodes nouvelles et modifiées, oui, parce que c'est applicable et que personne n'a à le planifier. En barrière à l'échelle du dépôt sur une base existante, non : cela produit un backlog plutôt qu'un changement de comportement.