Test sur-spécifié.
Un test sur-spécifié vérifie bien plus que ce qu'exige le comportement testé — figeant des chaînes de sortie exactes, des formes d'objet complètes, l'ordre d'une collection ou chaque appel de collaborateur interne — de sorte qu'il casse dès qu'un détail d'implémentation sans rapport change.
##Signs and Symptoms
Vous reconnaissez un test sur-spécifié à ce qui le casse : une refactorisation anodine ou un changement accessoire fait échouer des tests alors même que le comportement qu'ils décrivent reste correct.
Signaux courants :
- Assertions sur des détails non pertinents. Espaces/balisage exacts, chaînes de messages d'erreur complètes, texte de log, formatage, ou valeurs volatiles comme les horodatages, les UUID et les identifiants auto-incrémentés.
- Égalité d'objet entier alors qu'un seul champ compte.
toEqual({...l'objet entier...})alors que le test porte en réalité sur une seule propriété. - Assertions sensibles à l'ordre sur des données logiquement non ordonnées. Figer l'ordre d'un ensemble, des clés d'une map, ou des résultats d'une requête que le contrat ne garantit pas.
- Vérification du comportement de collaborateurs internes. Des mocks qui assertent des comptages d'appels exacts, des valeurs argument par argument et l'ordre d'appel de dépendances internes au lieu du résultat final observable.
- Snapshots géants qui capturent un arbre rendu entier ou un corps de réponse complet, de sorte que tout changement imbriqué force un réenregistrement.
- Plonger dans l'implémentation. Interroger l'état privé ou la structure du DOM (
container.querySelector, champs internes) plutôt que le comportement public, observable par l'utilisateur.
// Sur-spécifié : fige le balisage exact, un horodatage volatil et chaque appel de collaborateur
test('renders user badge', () => {
const html = renderBadge(user);
expect(html).toBe('<span class="badge badge--admin">Ada Lovelace</span>'); // balisage exact
expect(analytics.track).toHaveBeenCalledTimes(1); // nombre d'appels internes
expect(analytics.track).toHaveBeenCalledWith('badge_render', { ts: 1718445693221 }); // volatil
});
Un test décisif utile : si vous pouvez changer l'implémentation sans changer le comportement documenté et que le test échoue quand même, il est sur-spécifié.
##Reasons for the Problem
Pourquoi cela se produit
- Un état d'esprit « plus d'assertions = plus de rigueur » : les développeurs figent tout ce qu'ils voient, confondant complet et correct.
- L'outillage fait de la sur-spécification la voie de moindre résistance :
toMatchSnapshot()enregistre la sortie entière, et les frameworks de mock rendent trivial le fait d'asserter chaque interaction (toHaveBeenCalledWith, ordre des appels, comptages). - Copier-coller un littéral d'objet réel dans
toEqualau lieu de n'asserter que le champ qui concerne le test. - Tester via des points d'accès d'implémentation pratiques (nœuds du DOM, champs privés) plutôt que via le contrat public.
Pourquoi c'est nuisible
Dans xUnit Test Patterns de Gerard Meszaros, c'est la cause profonde du smell Test fragile, attribuée au logiciel sur-spécifié et à la sensibilité au comportement : les tests sont couplés à la manière dont le code fonctionne plutôt qu'à ce qu'il produit, si bien qu'ils échouent sur des changements qui n'affectent pas le comportement.
- Maintenabilité. Chaque refactorisation déclenche une cascade d'échecs de tests sans rapport, rendant la suite coûteuse à garder au vert et décourageant activement la refactorisation.
- Fiabilité / confiance. Les tests qui « crient au loup » sur du code correct entraînent l'équipe à ignorer les échecs ou à les réenregistrer aveuglément (surtout avec les snapshots).
- Fausse confiance. La sur-spécification s'accompagne souvent d'une sous-assertion du résultat significatif : un test peut figer un horodatage et une ligne de log sans jamais vérifier la valeur qui importe réellement à l'utilisateur. Comme le dit la littérature sur les tests d'interaction, la sur-spécification consiste à « vérifier des choses qui ne font pas partie du résultat final » — le plus souvent en assertant des interactions plutôt que des résultats.
- Lisibilité. Un mur d'assertions accessoires masque l'unique comportement que le test est censé documenter.
##Treatment
N'assertez que ce qu'exige le comportement testé — et préférez vérifier le résultat final plutôt que les interactions internes.
- Nommez l'unique résultat que le test documente et assertez-le, rien de plus. Scindez les résultats véritablement distincts en tests séparés et bien nommés, plutôt qu'en une seule méga-assertion.
- Utilisez des matchers partiels/souples au lieu de l'égalité exacte d'objet entier :
expect.objectContaining,toMatchObject,arrayContaining,stringContaining/stringMatching, etexpect.any(...)pour les champs que vous ne pouvez pas ou ne devriez pas figer. - Neutralisez les données volatiles (horodatages, identifiants, valeurs aléatoires) avec des matchers de propriété (
expect.any(String)) ou en injectant une horloge/un générateur d'identifiants fixe — ne codez pas en dur la valeur du jour. - Abandonnez les hypothèses d'ordre pour les données non ordonnées : assertez l'appartenance (
arrayContaining) ou triez avant de comparer. - Préférez la vérification d'état à la vérification de comportement. Stubbez les requêtes (ne les assertez pas) ; ne vérifiez l'appel d'un collaborateur que lorsque cet appel est l'effet de bord observable (une commande), et évitez d'asserter les comptages/l'ordre exacts de collaborateurs purement internes.
- Gardez les snapshots petits et intentionnels, ou remplacez un snapshot tentaculaire par quelques assertions ciblées. Interrogez via l'API destinée à l'utilisateur (par ex.
getByRole/getByTextde Testing Library) plutôt que par accès àcontainer/aux nœuds du DOM.
Avant → après :
// Avant : couple le test à des champs volatils et à l'ordonnancement des appels internes
expect(result).toEqual({
id: '8f3c-92a1-...', // uuid aléatoire
createdAt: '2026-06-15T10:01:33.221Z',// Date.now()
name: 'Ada Lovelace',
role: 'admin',
});
expect(logger.info).toHaveBeenCalledTimes(3);
expect(db.connect).toHaveBeenCalledBefore(db.query);
// Après : n'asserter que le comportement visé par ce test
expect(result).toMatchObject({ name: 'Ada Lovelace', role: 'admin' });
// id/createdAt sont accessoires ; la journalisation et l'ordre des appels sont des détails d'implémentation — ne les figez pas
##Detected by
- eslint-jest jest/no-large-snapshots — no-large-snapshots
- eslint-vitest vitest/no-large-snapshots — no-large-snapshots
- eslint-testing-library testing-library/no-node-access — no-node-access
- eslint-testing-library testing-library/no-container — no-container