Test obscur.
Un Test obscur est un test où le lecteur ne peut pas déterminer, à partir de la seule méthode de test, quel scénario est mis en place, quel comportement est exercé et quel résultat est attendu — parce que l'intention est enfouie sous trop de détails, trop peu de contexte, ou une logique cachée ailleurs.
##Signes et symptômes
Vous lisez un test et vous ne pouvez toujours pas répondre à trois questions : quelle est la configuration, qu'est-ce qui est exercé, et quel est le résultat attendu ? La chaîne cause/effet est cachée. Meszaros regroupe les causes en « trop d'informations » et « trop peu d'informations ». Indices courants :
- Test avide — une seule méthode de test vérifie de nombreux comportements sans rapport, si bien qu'aucune intention unique n'est claire.
- Invité mystère — la fixture ou les valeurs attendues vivent hors du test (un fichier, un enregistrement de BD partagé, une fixture chargée par son nom), de sorte que vous ne voyez pas pourquoi l'assertion devrait passer.
- Fixture générale — un gros
beforeEach/factory partagé construit bien plus que ce dont ce test a besoin ; les quelques champs pertinents se perdent dans le bruit. - Informations non pertinentes — des pages de données de configuration ou un snapshot géant alors qu'un ou deux champs seulement comptent réellement. Traité en détail dans Test verbeux.
- Données de test codées en dur — des littéraux magiques (
42,"a3f9-...",userId=7) sans signification nommée, répétés entre la configuration et les assertions. Voir Nombre magique dans les tests. - Test indirect — le test sollicite le système sous test à travers plusieurs autres objets, obscurcissant ce qui est réellement testé.
// Obscur : Avide + Invité mystère + Informations non pertinentes
test('user', async () => {
const data = loadFixture('users.json'); // invité mystère : les valeurs vivent dans un fichier
const svc = new UserService(data, cfg, clock); // fixture générale : la majeure partie est inutilisée
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(u.id).toBeDefined();
expect(u.email).toContain('@');
expect(await svc.count()).toBe(data.length + 1); // pourquoi ce nombre ? la réponse est dans le fichier
expect(await svc.login(u.email, 'p@ss')).toBe(true); // second comportement sans rapport
});
Ici vous ne pouvez pas dire ce que le test prouve sans ouvrir users.json, et il vérifie en réalité la création et la connexion à la fois.
##Causes du problème
Pourquoi cela se produit
- Les tests grossissent par accrétion : un développeur ajoute « une assertion de plus » à un test existant au lieu d'en écrire un nouveau, produisant un Test avide.
- Partager la configuration paraît efficace, alors une seule fixture large ou un
beforeEachest réutilisé partout (Fixture générale), et des fichiers externes/données de BD sont chargés par leur nom (Invité mystère). - Le copier-coller de données d'apparence réaliste traîne avec lui des dizaines de champs non pertinents et de nombres magiques.
- Le sur-mockage ou le passage par de nombreux collaborateurs transforme un test unitaire en Test indirect.
Pourquoi c'est nuisible
- Lisibilité : un test est la documentation du comportement attendu. Si le lecteur ne peut pas reconstituer Préparer → Exercer → Vérifier, cette valeur documentaire est perdue.
- Maintenabilité : quand un test obscur échoue, vous ne savez pas quel comportement a cassé ni si c'est le test ou le code qui a tort, si bien que les changements sont lents et risqués.
- Fiabilité : l'Invité mystère et la Fixture générale couplent le test à un état externe/partagé, provoquant des échecs instables (flaky) ou dépendants de l'ordre (et brisant la propriété « fraîche et déterministe » d'un bon test unitaire).
- Fausse confiance : les Tests avides masquent la couverture — un échec en début de méthode court-circuite les vérifications ultérieures, de sorte que des comportements que vous croyez testés ne s'exécutent peut-être jamais. Et comme le note Meszaros, les erreurs de codage sont plus faciles à cacher dans un test obscur, produisant des Tests bogués qui passent pour de mauvaises raisons.
##Traitement
Il n'existe pas de remède unique au test obscur, car « obscur » est un symptôme aux causes multiples. Diagnostiquez d'abord, puis appliquez le remède propre à la cause trouvée : chacune a son article avec les refactorings détaillés.
- Faites une lecture à froid. Demandez à quelqu'un qui n'a pas vu le code d'énoncer, à partir du seul corps du test, la mise en place, l'action et le résultat attendu. Là où il doit ouvrir un autre fichier ou deviner, se trouve la cause.
- Plusieurs comportements sans rapport dans un même test ? C'est un test avide (Eager Test) : scindez-le en tests ciblés, chacun nommé d'après le comportement qu'il vérifie. Un échec précoce cesse alors de masquer les vérifications suivantes.
- Des valeurs attendues justifiées par un fichier externe, un jeu de données initial ou un enregistrement partagé ? C'est un invité mystère : ramenez dans le test les données dont dépend l'assertion.
- Un
beforeEachpartagé et large construit bien plus que ce dont le test a besoin ? C'est une fixture générale : passez à une fixture minimale et fraîche pour chaque test. - Des pages de mise en place sans rapport ou des assertions champ par champ ? C'est un test verbeux : cachez le bruit derrière des méthodes de création et des builders, et comparez à un objet attendu — la mécanique y est détaillée.
- Des littéraux inexpliqués ? Voir Nombre magique dans les tests : nommez les valeurs pour que l'assertion s'explique d'elle-même.
- Le système sous test n'est atteint qu'à travers plusieurs autres objets (test indirect) ? Testez-le via sa propre interface, ou extrayez la logique pour pouvoir la tester directement.
// Avant (obscur)
test('user', async () => {
const data = loadFixture('users.json');
const svc = new UserService(data, cfg, clock);
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(await svc.count()).toBe(data.length + 1);
});
// Après : une seule intention, fixture en ligne, données nommées
test('create() persists a new user', async () => {
const svc = userServiceWith([]); // fixture minimale et fraîche
const newUser = aUser({ email: '[email protected]' }); // le builder fixe par défaut le bruit
const created = await svc.create(newUser);
expect(created.email).toBe('[email protected]');
expect(await svc.count()).toBe(1); // explicite, aucun fichier externe
});
Visez la forme AAA (Arrange-Act-Assert / Préparer-Agir-Vérifier) : un lecteur devrait saisir le scénario sans quitter la méthode de test.
##Détecté par
- eslint-jest jest/max-expects — Limite le nombre d’appels à expect() par test
- eslint-vitest vitest/max-expects — Limite le nombre d’appels à expect() par test
- eslint-jest jest/no-large-snapshots — Interdit les snapshots volumineux
- eslint-vitest vitest/no-large-snapshots — Interdit les snapshots volumineux
- eslint-jest jest/max-nested-describe — Limite la profondeur d’imbrication des blocs describe()