ConstructiCat Logo
CodeBust.
Browse section ▾

Tester les détails d'implémentation.

Un test vérifie le fonctionnement interne du code — champs privés, appels de méthodes internes, structure du DOM ou classes CSS — au lieu du comportement observable dont dépend un véritable consommateur.

##Signs and Symptoms

Vous reconnaissez cette odeur lorsqu'un test va au-delà du contrat public et fige la mécanique qui se trouve derrière. Indices courants :

  • Des assertions sur l'état interne/privé ou les champs, ou l'invocation directe de méthodes privées (souvent via des casts, de la réflexion ou // @ts-expect-error).
  • Espionner ou vérifier qu'une fonction utilitaire interne a été appelée (expect(internalCalc).toHaveBeenCalled()) au lieu de contrôler le résultat.
  • Des tests d'interface qui interrogent par classe CSS, balise, attributs data-* ou position dans le DOM plutôt que par rôle/libellé/texte — p. ex. container.querySelector('.btn-primary > span:nth-child(2)'), wrapper.state(), wrapper.find('SomeChildComponent').props().
  • Des tests par instantané (snapshot) portant sur des arbres de rendu entiers / des objets internes sérialisés, si bien que la moindre retouche du balisage les fait échouer.
  • Des tests qui cassent à chaque refactorisation alors que la fonctionnalité marche toujours (faux négatifs) et, à l'inverse, continuent de passer après un bug de logique parce qu'ils ne revérifient que le câblage (faux positifs).
// ODEUR : couple le test aux rouages internes du composant et à la structure du DOM
test('counter increments', () => {
  const wrapper = mount(<Counter />);
  wrapper.instance().handleClick();          // appelle directement une méthode privée
  expect(wrapper.state('count')).toBe(1);    // vérifie l'état interne
  expect(wrapper.find('.count-display').text()).toBe('1'); // sélecteur CSS fragile
});

La même forme apparaît côté serveur : expect(service._cache.size).toBe(1) ou la vérification de la séquence exacte des appels internes qu'effectue une méthode.

##Reasons for the Problem

Pourquoi cela se produit

  • L'état interne est facile à atteindre — un champ public, une fonction utilitaire exportée ou container.querySelector est juste là, tandis que solliciter le comportement réel demande davantage de mise en place.
  • La course aux métriques de couverture : tester chaque méthode privée au cas par cas donne une impression d'exhaustivité.
  • L'usage intensif des mocks pousse à vérifier "ce collaborateur a-t-il été appelé" plutôt que "la bonne chose s'est-elle produite."
  • Un outillage qui l'encourage : rendu superficiel (shallow rendering) / API instance() / state(), ou récupération de nœuds par nom de classe.

Pourquoi c'est nuisible

  • Maintenabilité / fragilité. C'est le Fragile Test de Meszaros provoqué par un Overspecified Software : le test fige un comportement que le consommateur n'a jamais exigé, si bien que des refactorisations inoffensives (renommer une méthode, restructurer le balisage, modifier un champ privé) cassent des tests verts sans raison réelle. Les tests deviennent une taxe sur la refactorisation plutôt qu'un filet de sécurité.
  • Fausse confiance (le danger central, selon Kent C. Dodds). Les tests de détails d'implémentation échouent dans les deux mauvaises directions : faux négatifs (le test passe au rouge alors que la fonctionnalité marche toujours) et faux positifs (le test reste vert alors que la fonctionnalité est cassée — vous avez vérifié le câblage, pas le résultat). Dans les deux cas, la suite cesse de vous dire la vérité.
  • Lisibilité. Le test documente comment le code est construit, et non ce qu'il garantit. Un lecteur ne peut pas déterminer quel comportement importe réellement, et le test ne sert plus d'exemple d'utilisation ni de spécification.
  • Couplage. Il fige les décisions de conception actuelles, décourageant précisément les refactorisations que les tests sont censés rendre sûres.

##Treatment

Testez à travers le contrat public — la même surface que touche un véritable appelant ou utilisateur — et vérifiez la sortie observable : valeurs de retour, erreurs levées, événements émis, état persisté ou interface rendue/visible.

  1. Identifiez le consommateur. Pour un module, c'est son API exportée ; pour un composant d'interface, c'est l'utilisateur (clics, saisie) et ce qu'il peut voir.
  2. Fournissez les entrées comme le ferait un consommateur, et non en appelant des méthodes privées. Déclenchez un vrai clic au lieu d'invoquer le gestionnaire ; appelez la méthode publique au lieu de la fonction utilitaire.
  3. Vérifiez les résultats, pas les rouages internes. Remplacez les contrôles sur state()/champ privé/toHaveBeenCalled par des contrôles sur ce qui ressort.
  4. Interrogez l'interface par l'accessibilité, pas par la structure — rôle, libellé, texte — plutôt que par des classes CSS, des balises ou nth-child.
  5. Cessez de tester directement les méthodes privées. Couvrez-les via la méthode publique qui les utilise ; si une unité privée est assez complexe pour mériter ses propres tests, c'est le signe qu'il faut l'extraire dans son propre module doté de sa propre API publique.
  6. Réservez les assertions sur les mocks/espions aux véritables frontières (réseau, temps, passerelle de paiement) où l'appel lui-même est le comportement observable — pas aux collaborateurs internes.
// AVANT : teste les détails d'implémentation
const wrapper = mount(<Counter />);
wrapper.instance().handleClick();
expect(wrapper.state('count')).toBe(1);
expect(wrapper.find('.count-display').text()).toBe('1');

// APRÈS : teste le comportement observable via le contrat public, orienté utilisateur
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();

Règle empirique : si une refactorisation préservant le comportement casse le test, c'est que le test vérifiait un détail d'implémentation.

##Detected by