Métriques de qualité du code

La métrique CRAP : complexité et couverture en un chiffre

CRAP signifie Change Risk Anti-Patterns. La métrique multiplie l'enchevêtrement d'une méthode par son défaut de tests, et le résultat répond à une question qu'aucune métrique seule ne traite : quel code est dangereux à modifier ?

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.

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.

CRAP(m) = CC² × (1 − cov)³ + CC
  • 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.

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

js

N'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

sh

Un 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.

# 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

js

Quand 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.

// 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;
}

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.

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 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, et il existe des implémentations communautaires pour Rust, .NET et Groovy, mais 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.

C'est suffisant pour l'intégrer à un build. Barrez le code nouveau et modifié à un seuil, triez le reste par score contre fréquence de modification, et lisez les deux entrées séparément pour que le chiffre se traduise en action : découper cette méthode, ou la tester. Gardez seulement en tête que la moitié couverture de la formule ne vaut que ce que vos assertions lui font valoir.

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.

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.

La métrique CRAP est-elle encore pertinente ?

L'outil Java d'origine dort depuis longtemps, mais la formule réapparaît sans cesse dans de nouveaux écosystèmes parce qu'elle est triviale à implémenter et répond à une question que les tableaux de bord mono-métrique ne traitent pas. Partout où vous collectez déjà couverture et complexité, vous en êtes à deux lignes.

Un score CRAP faible signifie-t-il que le code est sûr à modifier ?

Il signifie que la complexité est couverte, ce qui n'est pas la même chose que bien testée. La couverture compte l'exécution plutôt que l'assertion, donc un score faible bâti sur des tests faibles est un chiffre confortable posé sur une réalité inconfortable. Les tests de mutation sont la façon habituelle de vérifier que la couverture est réelle.

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.

© 2026 - Ryware.