Dette Technique et Tests : Comment le Refactoring Améliore la Qualité
Romain Lefebvre
6 mars 2026

Chaque équipe de développement accumule de la dette technique. C'est inévitable : les délais serrés, les changements de spécifications et l'évolution naturelle du code créent des compromis qui s'empilent au fil du temps. Le véritable enjeu n'est pas d'éliminer toute dette technique, mais de la gérer activement. Et pour cela, les tests automatisés sont votre meilleur allié.
Sans tests, le refactoring est un pari risqué. Avec une suite de tests solide, il devient un levier d'amélioration continue. Cet article explore le triangle vertueux entre dette technique, tests et refactoring, avec des stratégies concrètes pour améliorer la qualité de votre code de manière durable.
Qu'est-ce que la dette technique ?
Définition et métaphore
Le terme « dette technique » a été introduit par Ward Cunningham en 1992. Comme une dette financière, elle implique un emprunt (un raccourci dans le code) et des intérêts (le coût de maintenance croissant). Plus la dette s'accumule, plus chaque nouvelle fonctionnalité coûte cher à développer.
Les types de dette technique
Toutes les dettes techniques ne se valent pas. Martin Fowler propose une classification en deux axes :
| Délibérée | Involontaire | |
|---|---|---|
| Prudente | « Nous savons que cette approche est un compromis, nous y reviendrons au sprint suivant » | « Maintenant que nous comprenons mieux le domaine, nous voyons comment faire mieux » |
| Imprudente | « Pas le temps de tester, on livrera comme ça » | « C'est quoi le principe de responsabilité unique ? » |
La dette prudente et délibérée est parfois acceptable. La dette imprudente et involontaire est toxique et doit être combattue par la formation et les revues de code.
Symptômes de dette technique élevée
Reconnaître la dette technique est la première étape pour la traiter. Voici les signes les plus courants :
- Temps de développement croissant : Les fonctionnalités « simples » prennent de plus en plus de temps
- Bugs en cascade : Corriger un bug en crée d'autres
- Peur de modifier le code : Les développeurs évitent certaines zones du code
- Duplication massive : Le même motif copié-collé à de nombreux endroits
- Tests absents ou fragiles : Impossible de vérifier si une modification casse quelque chose
- Documentation obsolète : Le code ne correspond plus à la documentation
- Onboarding difficile : Les nouveaux développeurs mettent des semaines à comprendre le système
Le rôle des tests dans la gestion de la dette
Les tests comme filet de sécurité
Le refactoring sans tests est comme marcher sur un fil sans filet. Vous pouvez réussir, mais le risque de chute est élevé et les conséquences sont graves. Les tests automatisés offrent la garantie que votre refactoring ne modifie pas le comportement observable du système.
Si vous n'avez pas encore de tests en place, commencez par lire pourquoi tester est essentiel pour comprendre les fondamentaux.
Les tests comme détecteur de dette
Une suite de tests peut révéler la dette technique de plusieurs manières :
Tests difficiles à écrire : Si tester une fonction nécessite de mocker 15 dépendances, c'est un signe de couplage excessif. Le test ne pointe pas un problème de testabilité — il révèle un problème de conception.
Tests fragiles : Des tests qui cassent à chaque modification mineure indiquent un couplage fort entre les tests et les détails d'implémentation, souvent symptôme d'une architecture trop rigide.
Tests lents : Des tests unitaires qui prennent des secondes chacun suggèrent des dépendances cachées vers des ressources externes (base de données, réseau, système de fichiers). Pour maîtriser ces dépendances, consultez notre article sur les mocks et stubs.
Couverture en trous : Les zones du code sans couverture de test sont souvent les zones les plus anciennes et les plus endettées. C'est là que les bugs se concentrent. Pour comprendre comment interpréter ces métriques, lisez notre guide sur la couverture de code.
Le cercle vicieux dette-tests
La dette technique et l'absence de tests se renforcent mutuellement :
- Le code est mal structuré → difficile à tester
- Pas de tests → peur de refactorer
- Pas de refactoring → la dette s'accumule
- Plus de dette → encore plus difficile à tester
Briser ce cercle est l'enjeu principal. La solution : commencer petit, tester ce qui est testable, puis élargir progressivement.
Stratégies de refactoring avec tests
La règle du scout
Robert C. Martin (Uncle Bob) propose une règle simple : « Laissez le code plus propre que vous ne l'avez trouvé. » Chaque fois que vous touchez du code pour une fonctionnalité ou un correctif, améliorez un petit aspect : renommez une variable, extrayez une fonction, ajoutez un test.
Cette approche incrémentale est la plus durable. Elle ne nécessite pas de « sprint de dette technique » et s'intègre naturellement dans le travail quotidien.
Caractériser avant de refactorer
Avant de modifier du code legacy, la première étape est d'écrire des tests de caractérisation. Ces tests ne vérifient pas ce que le code devrait faire, mais ce qu'il fait réellement :
// Code legacy mal nommé et complexe
function proc(d, f) {
let r = 0;
for (let i = 0; i < d.length; i++) {
if (d[i].t === 'A') {
r += d[i].v * f;
} else if (d[i].t === 'B') {
r += d[i].v * f * 1.1;
} else {
r += d[i].v;
}
}
return Math.round(r * 100) / 100;
}
// Tests de caractérisation : on capture le comportement actuel
describe('proc (caractérisation)', () => {
it('doit retourner 0 pour un tableau vide', () => {
expect(proc([], 1.5)).toBe(0);
});
it('doit calculer avec type A', () => {
const data = [{ t: 'A', v: 100 }];
expect(proc(data, 1.2)).toBe(120);
});
it('doit appliquer le multiplicateur 1.1 pour type B', () => {
const data = [{ t: 'B', v: 100 }];
expect(proc(data, 1.0)).toBe(110);
});
it('doit utiliser la valeur brute pour les autres types', () => {
const data = [{ t: 'C', v: 50 }];
expect(proc(data, 2.0)).toBe(50);
});
it('doit arrondir à deux décimales', () => {
const data = [{ t: 'A', v: 33.33 }];
expect(proc(data, 1.1)).toBe(36.66);
});
it('doit combiner plusieurs éléments', () => {
const data = [
{ t: 'A', v: 100 },
{ t: 'B', v: 200 },
{ t: 'C', v: 50 }
];
expect(proc(data, 1.0)).toBe(370);
});
});
Une fois ces tests en place, vous pouvez refactorer en toute sécurité :
// Après refactoring : même comportement, code lisible
const MULTIPLIERS = {
A: (value, factor) => value * factor,
B: (value, factor) => value * factor * 1.1,
DEFAULT: (value) => value,
};
function calculateTotal(items, priceFactor) {
const total = items.reduce((sum, item) => {
const calculate = MULTIPLIERS[item.type] || MULTIPLIERS.DEFAULT;
return sum + calculate(item.value, priceFactor);
}, 0);
return Math.round(total * 100) / 100;
}
Les tests passent toujours ? Le refactoring est validé.
Le pattern Strangler Fig
Pour les systèmes plus larges, le pattern Strangler Fig (inspiré du figuier étrangleur) permet de remplacer progressivement du code legacy :
- Créer une nouvelle implémentation à côté de l'ancienne
- Router progressivement le trafic vers la nouvelle implémentation
- Vérifier avec des tests que les deux implémentations produisent les mêmes résultats
- Supprimer l'ancienne implémentation une fois la migration complète
// Étape 1 : Wrapper qui utilise les deux implémentations
function calculatePriceWithVerification(order) {
const legacyResult = legacyCalculatePrice(order);
const newResult = newCalculatePrice(order);
if (Math.abs(legacyResult - newResult) > 0.01) {
logger.error('Divergence détectée', {
orderId: order.id,
legacy: legacyResult,
new: newResult
});
return legacyResult; // Fallback sur l'ancien en cas de divergence
}
return newResult;
}
Extraire pour tester
La technique la plus courante pour rendre du code testable est l'extraction. Identifiez un bloc de logique au sein d'une fonction trop longue et extrayez-le en une fonction séparée, testable indépendamment.
Avant :
async function processOrder(orderId) {
const order = await db.orders.findById(orderId);
// ... 50 lignes de validation ...
// ... 30 lignes de calcul de prix ...
// ... 20 lignes d'envoi de notification ...
await db.orders.update(orderId, { status: 'processed' });
}
Après :
async function processOrder(orderId) {
const order = await db.orders.findById(orderId);
const validationErrors = validateOrder(order); // Testable
if (validationErrors.length > 0) throw new ValidationError(validationErrors);
const pricing = calculateOrderPricing(order); // Testable
await notifyOrderProcessed(order, pricing); // Testable
await db.orders.update(orderId, { status: 'processed', ...pricing });
}
// Chaque fonction extraite peut être testée unitairement
function validateOrder(order) { /* ... */ }
function calculateOrderPricing(order) { /* ... */ }
Mesurer la dette technique
Métriques quantitatives
Plusieurs métriques permettent de quantifier la dette technique :
Complexité cyclomatique : Mesure le nombre de chemins indépendants dans une fonction. Une complexité supérieure à 10 est un signal d'alerte, au-delà de 20 c'est critique.
# Avec ESLint
npx eslint --rule '{"complexity": ["error", 10]}' src/
Couplage afférent et efférent : Mesure les dépendances entrantes et sortantes d'un module. Un module avec beaucoup de dépendances entrantes est difficile à modifier (effet domino).
Churn rate : Identifie les fichiers modifiés le plus fréquemment. Les fichiers à haut churn et haute complexité sont les meilleurs candidats au refactoring.
# Top 10 des fichiers les plus modifiés
git log --format=format: --name-only | sort | uniq -c | sort -rn | head -10
Ratio dette/couverture : Les modules à haute complexité et faible couverture de test concentrent le risque.
Outils d'analyse statique
| Outil | Langages | Ce qu'il détecte |
|---|---|---|
| SonarQube | Multi-langage | Dette technique, bugs, vulnérabilités, duplication |
| ESLint | JavaScript/TypeScript | Patterns problématiques, complexité |
| CodeClimate | Multi-langage | Maintenabilité, duplication, complexité |
| Pylint | Python | Code smells, conventions, erreurs |
| PMD | Java | Code mort, complexité, mauvaises pratiques |
Tableau de bord de la dette
Créez un tableau de bord simple pour suivre l'évolution :
| Métrique | Valeur actuelle | Objectif | Tendance |
|---|---|---|---|
| Couverture de tests | 62 % | 80 % | En hausse |
| Complexité moyenne | 8.3 | < 6 | Stable |
| Duplication | 12 % | < 5 % | En baisse |
| Tests fragiles | 7 | 0 | En baisse |
| Temps moyen de build | 4 min 30 s | < 3 min | Stable |
TDD : prévenir plutôt que guérir
Le Test-Driven Development est la meilleure prévention contre la dette technique. En écrivant le test avant le code, vous garantissez :
- Une couverture naturellement élevée : Chaque ligne de code existe pour faire passer un test
- Un design émergent : Le code est structuré par les besoins des tests, ce qui favorise le découplage
- Un refactoring intégré : Le cycle Rouge-Vert-Refactor inclut une phase de nettoyage à chaque itération
Pour une introduction complète au TDD, consultez notre guide TDD.
Le cycle Rouge-Vert-Refactor
Le cycle fondamental du TDD est un mécanisme anti-dette intégré :
Rouge : Écrire un test qui échoue. Ce test décrit le comportement attendu.
test('doit calculer la TVA à 20%', () => {
expect(calculateVAT(100, 'FR')).toBe(20);
});
Vert : Écrire le minimum de code pour faire passer le test.
function calculateVAT(amount, country) {
return amount * 0.20;
}
Refactor : Améliorer le code sans changer le comportement. C'est cette phase qui empêche l'accumulation de dette.
const VAT_RATES = {
FR: 0.20,
DE: 0.19,
ES: 0.21,
// ...
};
function calculateVAT(amount, countryCode) {
const rate = VAT_RATES[countryCode];
if (!rate) throw new Error(`Taux de TVA inconnu pour ${countryCode}`);
return Math.round(amount * rate * 100) / 100;
}
La discipline du refactoring à chaque cycle empêche la dette de s'accumuler.
Stratégie de remboursement de la dette
Prioriser par impact
Toute la dette technique ne mérite pas d'être remboursée immédiatement. Priorisez selon deux critères :
- Fréquence de modification : Le code modifié souvent bénéficie le plus du refactoring
- Criticité métier : Le code qui gère les paiements mérite plus d'attention que le code d'administration
La matrice de priorisation :
| Modifié souvent | Rarement modifié | |
|---|---|---|
| Critique | Priorité maximale | Priorité haute |
| Non critique | Priorité moyenne | Priorité basse |
Allouer du temps régulièrement
Plutôt que des « sprints de dette technique » ponctuels, intégrez le remboursement dans chaque sprint :
- Règle des 20 % : Réservez 20 % du temps de chaque sprint au remboursement de la dette
- Règle du scout : Chaque pull request doit laisser le code un peu meilleur
- Journée technique mensuelle : Une journée dédiée aux refactorings plus importants
Documenter les décisions
Quand vous contractez délibérément de la dette technique, documentez-la :
// TECH-DEBT: Ce calcul utilise une approximation linéaire au lieu de la
// formule exacte. Impact : erreur < 0.5% pour les montants < 10000€.
// Ticket de suivi : JIRA-1234
// Date de revue prévue : prochain sprint de consolidation
function approximateInterest(principal, rate, years) {
return principal * rate * years; // Approximation simple vs composé
}
Refactoring : les techniques essentielles
Renommer pour clarifier
Le refactoring le plus simple et le plus impactant. Un bon nom élimine le besoin de commentaire :
// Avant
const d = calcP(u.a, u.b, f);
// Après
const discountedPrice = calculateDiscountedPrice(
user.purchaseHistory,
user.membershipLevel,
currentPromotionFactor
);
Éliminer la duplication
La duplication est la forme la plus courante de dette technique. Chaque duplication est une divergence en puissance.
// Avant : logique dupliquée dans 3 contrôleurs
// controllers/orders.js
const isValid = email && email.includes('@') && email.length > 5;
// controllers/users.js
const emailValid = email && email.indexOf('@') > -1 && email.length >= 6;
// controllers/newsletter.js
const ok = email?.includes('@') && email.length > 4;
// Après : une seule source de vérité
// validators/email.js
export function isValidEmail(email) {
if (!email || typeof email !== 'string') return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
Remplacer les conditions par le polymorphisme
Les longues chaînes de if/else ou switch sont souvent le signe d'un manque de polymorphisme :
// Avant : switch grandissant à chaque nouveau type
function calculateShipping(order) {
switch (order.shippingMethod) {
case 'standard':
return order.weight * 0.5 + 4.99;
case 'express':
return order.weight * 1.2 + 9.99;
case 'overnight':
return order.weight * 2.0 + 19.99;
case 'international':
return order.weight * 3.5 + 29.99;
// ... de plus en plus de cas
}
}
// Après : stratégie extensible
const shippingStrategies = {
standard: { weightRate: 0.5, basePrice: 4.99 },
express: { weightRate: 1.2, basePrice: 9.99 },
overnight: { weightRate: 2.0, basePrice: 19.99 },
international: { weightRate: 3.5, basePrice: 29.99 },
};
function calculateShipping(order) {
const strategy = shippingStrategies[order.shippingMethod];
if (!strategy) throw new Error(`Méthode inconnue : ${order.shippingMethod}`);
return order.weight * strategy.weightRate + strategy.basePrice;
}
Introduire des objets de valeur
Les types primitifs (chaînes, nombres) utilisés pour représenter des concepts métier sont source d'erreurs :
// Avant : des chaînes partout, aucune validation
function createInvoice(amount, currency, customerId) {
// amount est-il en centimes ou en euros ?
// currency est-il "EUR", "eur", "€" ?
// customerId est-il validé ?
}
// Après : objets de valeur avec validation intégrée
class Money {
constructor(amountInCents, currency) {
if (!Number.isInteger(amountInCents) || amountInCents < 0) {
throw new Error('Le montant doit être un entier positif en centimes');
}
if (!['EUR', 'USD', 'GBP'].includes(currency)) {
throw new Error(`Devise non supportée : ${currency}`);
}
this.amountInCents = amountInCents;
this.currency = currency;
}
toDisplay() {
return `${(this.amountInCents / 100).toFixed(2)} ${this.currency}`;
}
add(other) {
if (this.currency !== other.currency) {
throw new Error('Impossible d\'additionner des devises différentes');
}
return new Money(this.amountInCents + other.amountInCents, this.currency);
}
}
L'intelligence artificielle au service du refactoring
Les outils d'IA peuvent accélérer considérablement le refactoring, mais ils ne remplacent pas le jugement humain. Pour comprendre comment l'IA transforme les pratiques de test, consultez notre article sur l'IA et les tests.
Ce que l'IA fait bien
- Suggérer des noms plus clairs pour les variables et fonctions
- Détecter les patterns de duplication sur de grandes bases de code
- Générer des tests de caractérisation pour du code legacy
- Proposer des refactorings structurels (extraction, simplification)
Ce que l'IA fait mal
- Comprendre l'intention métier derrière le code
- Évaluer l'impact d'un refactoring sur l'architecture globale
- Prioriser quoi refactorer en premier
- Garantir l'équivalence comportementale sans tests
L'IA est un outil d'accélération, pas de décision. Utilisez-la pour générer des suggestions, mais validez toujours avec votre suite de tests et votre jugement technique.
Plan d'action concret
Pour une équipe souhaitant commencer à rembourser sa dette technique :
Semaine 1 : Diagnostic
- Mesurer la couverture de code actuelle
- Identifier les 10 fichiers les plus modifiés (
git log) - Croiser avec la complexité cyclomatique
- Lister les zones critiques sans tests
Semaine 2-3 : Fondations
- Configurer les outils de mesure (SonarQube ou équivalent)
- Mettre en place le ratcheting de couverture dans la CI
- Écrire des tests de caractérisation pour les 3 modules les plus critiques
Semaine 4+ : Remboursement progressif
- Appliquer la règle du scout à chaque PR
- Allouer 20 % du sprint au refactoring
- Suivre les métriques mensuellement
- Célébrer les améliorations pour maintenir la motivation
Pour intégrer ces pratiques dans votre pipeline d'intégration continue, consultez notre guide sur CI/CD et tests.
Conclusion
La dette technique n'est pas une fatalité. C'est une réalité à gérer activement, et les tests automatisés sont l'outil indispensable pour le faire. Sans tests, le refactoring est un risque. Avec des tests, c'est un investissement rentable.
Les principes à retenir :
- Mesurez votre dette avant de la rembourser : couverture, complexité, churn
- Caractérisez le code legacy avec des tests avant de le modifier
- Refactorez de manière incrémentale, intégrée au travail quotidien
- Prévenez avec le TDD : le cycle Rouge-Vert-Refactor est un mécanisme anti-dette
- Priorisez le code critique et fréquemment modifié
- Automatisez les garde-fous dans votre pipeline CI/CD
Le refactoring n'est pas un luxe réservé aux projets parfaits. C'est une pratique essentielle pour tout projet qui vise à durer. Commencez petit, mesurez vos progrès, et construisez une culture d'amélioration continue au sein de votre équipe.
