ConstructiCat Logo
CodeBust.
Browse section ▾

Duplication de code de test.

La duplication de code de test survient lorsque le même code de mise en place, d’action ou d’assertion est copié-collé dans de nombreux tests, de sorte qu’un seul changement impose des modifications à de multiples endroits et que les tests pourrissent en copies fragiles et quasi identiques.

##Signs and Symptoms

Vous reconnaissez cette odeur lorsque les tests semblent avoir été écrits par copier-coller plutôt que par réutilisation :

  • La même construction de fixture/d’objet est reconstruite mot pour mot en tête de test après test.
  • Des séquences d’assertions identiques (les mêmes 3–4 appels à expect dans le même ordre) reviennent d’un test à l’autre.
  • Les nouveaux tests sont manifestement clonés d’un ancien avec une seule ligne modifiée.
  • Les mêmes littéraux magiques (identifiants, URL, dates, chaînes d’erreur) apparaissent encore et encore.
  • Un changement d’un seul constructeur ou d’une signature d’API casse des dizaines de tests à la fois (chirurgie au fusil de chasse).
  • Vous voyez des tests quasi dupliqués qui ne diffèrent que par les valeurs d’entrée/attendues — un candidat évident pour un test paramétré/par table.
test('flight can be cancelled', () => {
  const airport = new Airport('YYC', 'Calgary');                              // dupliqué
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // dupliqué
  flight.cancel();
  expect(flight.status).toBe('CANCELLED');
});

test('flight can be delayed', () => {
  const airport = new Airport('YYC', 'Calgary');                              // copier-coller
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // copier-coller
  flight.delay(30);
  expect(flight.status).toBe('DELAYED');
});

##Reasons for the Problem

Pourquoi cela arrive

  • Copier-coller le test précédent est la façon la plus rapide d’écrire le suivant.
  • Les tests sont traités comme des « citoyens de seconde zone » — ni refactorés ni soumis au même standard DRY que le code de production.
  • Il n’existe pas de fixtures partagées, de méthodes de création (Creation Methods) ou de builders de données de test, et les auteurs ne connaissent pas les tests paramétrés/pilotés par table.

Pourquoi c’est nuisible

  • Maintenabilité : Meszaros relie directement cette odeur au test fragile (Fragile Test) — lorsque « les mêmes séquences de code apparaissent de nombreuses fois dans de nombreux tests », un seul changement de production impose de modifier la même chose à N endroits. Le coût de maintenance croît avec le nombre de copies, et non avec le nombre de comportements distincts.
  • Lisibilité : le code répétitif (boilerplate) ensevelit l’unique ligne qui distingue réellement chaque test, si bien que les lecteurs ne voient pas rapidement ce qui est vérifié.
  • Fiabilité : le copier-coller invite aux erreurs de copier-coller, et les corrections sont appliquées à une copie mais pas à ses semblables, laissant des tests incohérents et contradictoires.
  • Fausse confiance : une assertion défectueuse qui a été dupliquée est désormais fausse à de nombreux endroits à la fois, et les tests clonés dérivent silencieusement jusqu’à ne plus exercer ce que leurs noms prétendent.

Mise en garde — DRY vs DAMP : les tests gagnent aussi à être des phrases descriptives et significatives (Descriptive And Meaningful Phrases). N’abstrayez pas à l’excès au point qu’un lecteur doive courir après des helpers pour comprendre un test. Extrayez la duplication véritable et révélatrice de l’intention ; gardez visible et local le détail essentiel propre à chaque test.

##Treatment

Éliminez la duplication accidentelle tout en gardant évidente l’essence de chaque test :

  1. Extrayez des utilitaires de test / des méthodes de création (Creation Methods) (Object Mother, Test Data Builder) pour la construction d’objets répétée, afin que chaque test ne nomme que les valeurs qui l’intéressent.
  2. Utilisez beforeEach / une mise en place implicite (Implicit Setup) pour le contexte véritablement partagé et pertinent pour chaque test du bloc — mais évitez de cacher un état dont un test dépend (cela troquerait la duplication contre un test obscur (Obscure Test)).
  3. Extrayez des assertions personnalisées / des helpers de vérification pour les séquences d’assertions multi-étapes récurrentes, en vérifiant idéalement une seule condition logique.
  4. Fusionnez les tests quasi identiques en tests paramétrés/pilotés par table (it.each / test.each), afin que les paires entrée + sortie attendue vivent dans une seule table.
  5. Remplacez les littéraux magiques dupliqués par des constantes nommées ou des valeurs par défaut de builder.

Avant — construction dupliquée et trois tests quasi identiques :

test('rejects negative amount', () => {
  expect(() => validateAmount(-1)).toThrow(RangeError);
});
test('rejects zero amount', () => {
  expect(() => validateAmount(0)).toThrow(RangeError);
});
test('rejects NaN amount', () => {
  expect(() => validateAmount(NaN)).toThrow(RangeError);
});

Après — une méthode de création supprime la duplication de mise en place, et une table remplace les clones :

// méthode de création partagée : les tests n'indiquent que les surcharges qui importent
const aFlight = (overrides = {}) =>
  new Flight('AC123', new Airport('YYC', 'Calgary'),
             new Date('2026-06-01T10:00'), overrides);

it.each([-1, 0, NaN])('rejects invalid amount %p', (amount) => {
  expect(() => validateAmount(amount)).toThrow(RangeError);
});

Exécutez un détecteur de copier-coller (ci-dessous) sur vos sources de test, puis refactorez en premier les blocs les plus volumineux/les plus répétés.

##Detected by

  • eslint-sonarjs no-identical-functionsLes fonctions ne devraient pas avoir des implémentations identiques
  • eslint-sonarjs no-duplicate-stringLes littéraux de chaîne ne devraient pas être dupliqués
  • sonar javascript:S4144Les fonctions ne devraient pas avoir des implémentations identiques
  • pmd cpdCopy/Paste Detector (CPD) — blocs de code dupliqués