Eager Test.
Un Eager Test vérifie plusieurs méthodes ou comportements distincts de l'unité testée dans une seule méthode de test, au lieu de se concentrer sur un seul comportement.
##Signs and Symptoms
Une seule méthode de test sollicite de nombreuses méthodes de production sans rapport et vérifie chacune d'elles, faisant parcourir à l'objet toute une séquence d'opérations au lieu de contrôler un seul résultat.
Signes révélateurs :
- Un nom de test vague et fourre-tout (
testUserService,it('works')) ou un nom assemblé avec des "et" (creates_and_renames_and_deletes). - Plusieurs cycles act → assert dans un même corps : vous appelez une méthode, vérifiez, en appelez une autre, vérifiez à nouveau.
- Des commentaires utilisés comme en-têtes de section à l'intérieur du test (
// on teste maintenant delete) pour séparer les choses vérifiées. - Les assertions touchent plusieurs méthodes/champs différents du SUT qui ne partagent pas un seul résultat logique.
// ODEUR : un seul test vérifie create, rename, delete et list
test('user service', () => {
const svc = new UserService();
const user = svc.create({ name: 'Ada' }); // comportement 1
expect(user.id).toBeDefined();
svc.rename(user.id, 'Grace'); // comportement 2
expect(svc.get(user.id).name).toBe('Grace');
svc.delete(user.id); // comportement 3
expect(svc.get(user.id)).toBeUndefined();
expect(svc.list()).toHaveLength(0); // comportement 4
});
Notez la distinction : un Eager Test consiste à tester plusieurs comportements, et non simplement à avoir plusieurs expect. Plusieurs assertions sur un seul résultat logique (p. ex. vérifier plusieurs champs du même objet renvoyé) sont acceptables et ne constituent pas cette odeur.
##Reasons for the Problem
Pourquoi cela se produit
- Réutilisation de la préparation / paresse. Préparer le fixture est fastidieux ou coûteux ; il est donc tentant de continuer à solliciter l'objet déjà construit et de vérifier "tant qu'on y est."
- Raisonnement par flux. L'auteur teste un parcours utilisateur ("créer, puis modifier, puis supprimer") comme un seul script au lieu d'isoler chaque comportement unitaire.
- Dérive du TDD. Un test parti d'une intention ciblée accumule des assertions supplémentaires à mesure que de nouvelles fonctionnalités lui sont greffées au lieu d'avoir leur propre test.
Pourquoi c'est nuisible
- Fausse confiance (le point majeur). La plupart des bibliothèques d'assertion interrompent le test à la première assertion qui échoue. Si
createcasse, les vérifications derename,deleteetlistne s'exécutent jamais — un passage du vert au rouge masque donc le nombre réel de comportements cassés, et un test qui passait n'exerçait en réalité jamais les étapes ultérieures dès qu'une étape antérieure régressait. - Diagnostics médiocres. Un échec vous dit "le test du service utilisateur a échoué", mais pas quel comportement. Vous devez lire toute la méthode pour localiser l'étape défaillante.
- Intention obscurcie / lisibilité. Le test ne documente plus un fait unique sur le système ; le lecteur doit le découper mentalement en les comportements qu'il regroupe.
- Fragilité et couplage. Les assertions ultérieures dépendent des mutations antérieures, si bien qu'une modification sans rapport en haut se répercute en cascade et rend le test fragile et difficile à refactoriser.
- Maintenabilité. Il est plus difficile de supprimer, déplacer ou renommer la couverture d'un comportement lorsqu'elle est enchevêtrée avec trois autres dans une même méthode.
##Treatment
Découpez l'eager test en plusieurs tests ciblés — un comportement par test — et remontez l'étape d'arrangement partagée dans la préparation afin que le découpage ne duplique pas le code répétitif.
Étapes :
- Listez les comportements que le test regroupe (ici : create, rename, delete, list-après-delete).
- Extrayez un test par comportement, chacun portant un nom descriptif qui révèle l'intention.
- Déplacez la préparation partagée dans un
beforeEachou une fabrique/Creation Method afin que chaque test dispose encore d'un SUT propre sans code d'arrangement copié-collé. - Conservez un seul Act par test et ne vérifiez que le résultat de ce comportement (plusieurs
expectsur le même résultat sont acceptables). - Si vous avez réellement besoin de valider un flux de bout en bout, conservez-le comme un seul test de scénario/d'intégration explicitement nommé — mais couvrez tout de même chaque comportement unitaire dans son propre test plutôt que de vous reposer sur le flux pour la couverture.
- Protégez-vous des régressions en activant une règle de nombre maximal d'assertions (voir les détecteurs) comme indicateur peu coûteux.
// APRÈS : tests ciblés, préparation partagée
let svc;
beforeEach(() => { svc = new UserService(); });
test('create() assigns an id', () => {
expect(svc.create({ name: 'Ada' }).id).toBeDefined();
});
test('rename() updates the stored name', () => {
const { id } = svc.create({ name: 'Ada' });
svc.rename(id, 'Grace');
expect(svc.get(id).name).toBe('Grace');
});
test('delete() removes the user', () => {
const { id } = svc.create({ name: 'Ada' });
svc.delete(id);
expect(svc.get(id)).toBeUndefined();
});
Désormais, n'importe quel comportement peut échouer indépendamment, le nom du test défaillant pointe précisément la rupture, et chaque test se lit comme un fait documenté sur le SUT.
##Detected by
- eslint-jest jest/max-expects — Imposer un nombre maximal d'appels à expect() par test
- eslint-vitest vitest/max-expects — Imposer un nombre maximal d'expect par test