Invité mystère.
Un test dont les entrées ou les résultats attendus se trouvent dans une ressource externe — un fichier, un jeu de données de base de données (seed) ou une fixture partagée — de sorte que vous ne pouvez ni comprendre ni faire confiance au test en le lisant seul.
##Signs and Symptoms
Vous ne pouvez pas comprendre un test en le lisant : les données qui le pilotent (et qui justifient ses assertions) se trouvent quelque part hors champ — un fichier CSV/JSON, un script de seed de base de données, un module de fixture partagé, ou une annotation de fixture de données du framework. Le test référence une ressource opaque puis fait des assertions sur des valeurs « magiques » dont la signification est cachée dans cette ressource.
Signes révélateurs :
- Le corps appelle quelque chose comme
readFileSync('fixtures/subscribers.csv'),loadSeed('invoices.sql'),getResource(...), ou unbeforeAllglobal qui alimente une base de données partagée. - Les valeurs attendues semblent arbitraires (
toHaveLength(3), un id/e-mail spécifique) et vous devez ouvrir un autre fichier pour apprendre pourquoi. - Une fixture partagée/« générale » est réutilisée par de nombreux tests, de sorte que les entrées pertinentes pour ce test sont noyées parmi des données dont il n'a que faire.
- Les tests se cassent lorsqu'un coéquipier modifie une fixture partagée pour un test sans rapport.
test('returns active subscribers', async () => {
// Invité mystère : quelles lignes contient ce fichier ? pourquoi 3 est-il correct ?
const subscribers = await loadSubscribersFromCsv('./fixtures/subscribers.csv');
const result = filterActive(subscribers);
expect(result).toHaveLength(3); // la justification se trouve en dehors du test
});
L'exemple original du catalogue de Meszaros/des Test Smells a la même forme : loadAirportsAndFlightsFromFile("test-flights.csv") suivi de assertEquals(1, flightsAtOrigin.size()) — le "1" n'a de sens que si vous lisez test-flights.csv.
##Reasons for the Problem
Pourquoi cela arrive
- Le principe DRY poussé trop loin. Les données de test sont extraites dans un fichier partagé ou une « fixture générale » pour éviter la duplication, échangeant la lisibilité contre la réutilisation.
- La commodité des données réelles. Déverser un CSV/JSON de production ou un
seed.sqlsemble plus facile que de construire des objets en ligne. - Configuration héritée/d'intégration. Les suites qui démarrent une base de données partagée ou s'appuient sur des annotations de fixtures de données du framework (
@magentoDataFixture …) héritent d'un état caché par défaut.
Pourquoi cela nuit
- Lisibilité / cause à effet. Le lien entre l'entrée et la sortie attendue est rompu. Un lecteur ne peut pas voir pourquoi l'assertion est correcte sans quitter le test, ce qui va à l'encontre du rôle d'un test en tant que documentation exécutable.
- Fausse confiance. Vous ne savez pas réellement ce que le test exerce ; le fichier peut contenir plus (ou moins) que ce que vous supposez, de sorte qu'une exécution réussie prouve moins qu'il n'y paraît.
- Fiabilité / déterminisme. La ressource externe peut être manquante, renommée, reformatée, ou différer selon l'environnement, le système d'exploitation, l'encodage ou la locale — les échecs reflètent alors l'état de la fixture, et non un véritable défaut (tests instables).
- Maintenabilité / couplage. Lorsque plusieurs tests partagent une même ressource, quiconque la modifie pour un test peut silencieusement casser les autres, et personne ne sait de quels champs chaque test dépend.
##Treatment
Rendez l'entrée pertinente visible à l'intérieur du test, à côté de l'assertion qui en dépend (une Fresh Fixture / configuration en ligne). L'objectif est que la valeur attendue devienne évidente.
Étapes concrètes :
- Inlinez les données qui comptent. Construisez dans le corps du test les quelques objets/lignes qui intéressent le test, afin que la valeur attendue de l'assertion soit manifestement correcte.
- Si vous avez réellement besoin d'un fichier, construisez-le dans le test. Utilisez une fonction utilitaire qui ne prend que les paramètres significatifs et écrit dans un chemin temporaire, puis nettoyez — de sorte que les valeurs significatives apparaissent dans le test, et non dans un blob versionné.
- Remplacez les fixtures partagées/« générales » par des fixtures propres à chaque test, ou exposez des créateurs/chercheurs au nom explicite (
createProductWithName('Simple Product'),getRecentlyAddedProduct()) afin que les attributs testés soient explicites tandis que la configuration sans rapport reste cachée derrière un constructeur bien nommé — et non derrière une ressource opaque.
Avant → après :
// Avant — Invité mystère : les données et le "2" sont cachés dans un fichier seed
test('flags overdue invoices', async () => {
await seedDatabaseFromFixture('invoices.sql');
const overdue = await findOverdueInvoices();
expect(overdue).toHaveLength(2);
});
// Après — les entrées sont visibles ; le résultat attendu est évident
test('flags overdue invoices', async () => {
await insertInvoice({ id: 1, dueDate: '2020-01-01', paid: false }); // en retard
await insertInvoice({ id: 2, dueDate: '2099-01-01', paid: false }); // pas encore échu
const overdue = await findOverdueInvoices();
expect(overdue.map(i => i.id)).toEqual([1]);
});
Pour le cas du fichier, préférez une fonction utilitaire ciblée à un fichier versionné :
const csv = makeSubscriberCsv(tmpFile, 'active@x.com', 'active@y.com', 'active@z.com');
// désormais, "3 actifs" est justifié par ce que vous voyez dans le test, et non par un fichier caché