ConstructiCat Logo
CodeBust.
Browse section ▾

Optimisme sur les ressources.

L'optimisme sur les ressources, c'est lorsqu'un test suppose qu'une ressource externe (un fichier, un répertoire, une table de base de données, une variable d'environnement ou un point d'accès réseau) existe déjà et se trouve dans un état connu, au lieu de la provisionner et de la vérifier, ce qui fait réussir ou échouer le test de façon non déterministe.

##Signs and Symptoms

Un test touche quelque chose en dehors de lui-même — un fichier, un chemin temporaire, un répertoire, une ligne de base de données, une variable d'environnement ou un point d'accès distant — et présume simplement qu'il est déjà là et correctement formé. Il n'y a aucune étape de préparation qui crée la ressource ni aucune vérification de son existence avant son utilisation.

Signes révélateurs :

  • Un chemin codé en dur est lu ou écrit sans existsSync/mkdir/writeFile préalable : par exemple fs.readFileSync('/tmp/app/config.json').
  • Le test passe sur votre machine ou à la première exécution, puis échoue sur un checkout neuf, en CI, dans un répertoire temporaire d'un autre OS, ou lorsque la suite s'exécute dans un ordre différent ou en parallèle.
  • La ressource dont il a besoin est en réalité créée par un autre test (ou une étape manuelle/réservée au dev), de sorte que le test ne fonctionne que comme effet de bord de l'ordre d'exécution.
  • Il ouvre un fichier/une connexion et assertte sur le résultat sans jamais vérifier que la ressource était présente — une ressource manquante lève une exception avant la véritable assertion, ou produit silencieusement des données vides qui « passent » quand même.
test('parses the config file', () => {
  // optimiste : suppose que /tmp/app/config.json existe déjà et est bien formé
  const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
  expect(JSON.parse(raw).port).toBe(8080);
});

L'heuristique de détection canonique (testsmells.org / tsDetect) : un test utilise une ressource de type File sans appeler au préalable une vérification d'existence/de validité telle que exists(), isFile() ou notExists().

##Reasons for the Problem

Pourquoi cela arrive

  • La ressource était présente au moment où le test a été écrit (un fichier de fixture dans le dépôt, une base de dev pré-remplie, un fichier temporaire laissé par une étape précédente), si bien que l'auteur ne ressent jamais son absence.
  • Il est simplement plus court de pointer vers un chemin ou une table existante que d'en allouer et d'en pré-remplir une dans la préparation, puis de la démanteler ensuite.
  • Un copier-coller depuis un autre test qui s'appuyait déjà sur un état partagé et ambiant.

Pourquoi c'est nuisible

  • Non-déterminisme / instabilité. Le résultat dépend de l'état de l'environnement, et non du code testé. van Deursen et al. décrivent exactement cela : des tests qui « tournent bien à un moment et échouent lamentablement à un autre ». Les runners CI vierges, les checkouts neufs, les workers parallèles et les emplacements temporaires différents selon l'OS sont là où ça fait mal.
  • Fausse confiance. Un test vert peut réussir à cause d'un état résiduel d'une exécution précédente ou d'un test précédent, et non parce que le code actuel est correct — ou bien une ressource manquante lève une exception trop tôt et l'assertion significative ne s'exécute jamais.
  • Couplage caché et dépendance à l'ordre. Lorsqu'un test crée ce qu'un autre consomme, la suite comporte un contrat d'ordonnancement invisible qui se brise sous le mélange (shuffling) ou le partitionnement (sharding).
  • Difficile à reproduire et à maintenir. Les échecs ne peuvent pas être reproduits localement car ils dépendent d'un état ambiant propre à la machine ; le débogage est donc lent et le test érode la confiance.

##Treatment

Faites en sorte que chaque test possède et contrôle chaque ressource qu'il touche, et ne présumez jamais d'un état ambiant.

  1. Provisionnez dans la préparation, nettoyez dans le démantèlement. Utilisez Setup External Resource — allouez et initialisez fichiers, répertoires, tables et connexions de base de données dans beforeEach/beforeAll, et libérez-les dans afterEach/afterAll pour que la prochaine exécution démarre propre.
  2. Créez, ne présumez pas. Écrivez le fichier, pré-remplissez la table ou démarrez le serveur bouchon dans la préparation. Si vous devez vraiment consommer une ressource préexistante, vérifiez d'abord son existence pour qu'une ressource manquante échoue bruyamment avec un message clair au lieu de corrompre la véritable assertion.
  3. Isolez par test. Utilisez un répertoire temporaire unique (fs.mkdtemp(os.tmpdir() + …)) ou un schéma/espace de noms neuf par test plutôt qu'un chemin partagé codé en dur, afin que les exécutions parallèles et les réexécutions n'entrent pas en collision.
  4. Mieux encore, supprimez la dépendance. Remplacez la ressource réelle par un mock ou un faux en mémoire (fs simulé, base en mémoire, HTTP bouchonné) afin que le test soit entièrement autonome et déterministe — la remédiation que recommande testsmells.org.

Avant → après :

// avant — Optimisme sur les ressources
test('parses the config file', () => {
  const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
  expect(JSON.parse(raw).port).toBe(8080);
});
// après — le test possède et vérifie sa ressource
import { mkdtemp, writeFile, rm, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

let dir;
beforeEach(async () => {
  dir = await mkdtemp(path.join(os.tmpdir(), 'cfg-'));
  await writeFile(path.join(dir, 'config.json'), JSON.stringify({ port: 8080 }));
});
afterEach(() => rm(dir, { recursive: true, force: true }));

test('parses the config file', async () => {
  const raw = await readFile(path.join(dir, 'config.json'), 'utf8');
  expect(JSON.parse(raw).port).toBe(8080);
});

##Detected by