---
title: "Test erratique (instable)"
type: "test-smell"
slug: "erratic-test"
url: "http://localhost:3000/fr/test-smells/erratic-test.md"
category: "Odeurs erratiques"
description: "Un test erratique (instable) passe et échoue par intermittence sur le même code parce que son résultat dépend du timing, de l'ordre, d'un état partagé ou d'autres facteurs non déterministes plutôt que du comportement testé."
---
# Test erratique (instable)

> Un test erratique (instable) passe et échoue par intermittence sur le même code parce que son résultat dépend du timing, de l'ordre, d'un état partagé ou d'autres facteurs non déterministes plutôt que du comportement testé.

## Signs and Symptoms

Un test **vert à une exécution et rouge à la suivante sans aucun changement de code** est erratique. On le reconnaît au comportement humain qui l'entoure autant qu'au code : les développeurs relancent le CI « pour qu'il passe », ajoutent `@Flaky`/`retry(3)`, ou mettent le test en quarantaine au lieu de le corriger.

Indices courants dans le code et le motif d'échec :

* **Timing/asynchrone :** des « attentes » `sleep`/`setTimeout`, des promesses non attendues, ou des assertions qui font la course avec le système testé. Les échecs corrèlent avec la vitesse de la machine ou la charge du CI.
* **Dépendance à l'ordre :** le test passe isolément mais échoue dans la suite complète, ou inversement. Mélanger l'ordre des tests change le résultat.
* **État mutable partagé :** collections statiques, ligne de base de données réutilisée, singleton, ou fixture mutée par un test antérieur.
* **Entrées non déterministes :** `Date.now()`/`new Date()`, `Math.random()`, locale/fuseau horaire, ordre d'itération de hash-map, ou identifiants auto-générés.
* **Ressources externes :** réseau, système de fichiers ou horloge réels. Les échecs ressemblent à des timeouts ou « connection refused », pas à des décalages d'assertion.
* **Assertions conditionnelles :** `expect` caché dans un `if`/`catch`/callback, si bien que le test passe silencieusement quand la branche ne s'exécute jamais.

```js
// Odeur : une « attente » codée en dur, un tableau partagé et une horloge réelle
const created = [];                       // partagé entre les tests → tests interagissants

test('shows a fresh receipt', async () => {
  render(<Checkout />);
  fireEvent.click(screen.getByText('Pay'));
  await new Promise(r => setTimeout(r, 300));   // on espère que la requête est terminée
  created.push('order-1');                       // fuit vers les tests suivants
  expect(screen.getByRole('status'))
    .toHaveTextContent(`Paid ${new Date().toISOString()}`); // change à chaque exécution
});

```

Cela correspond directement aux sous-odeurs du _Test erratique_ de Meszaros : **Tests interagissants / Guerre des exécutions de tests** (état partagé), **Test solitaire** (ne s'exécute qu'après un autre), **Optimisme sur les ressources** (suppose qu'une ressource externe est présente), **Fuite de ressources** (ne nettoie pas) et **Test non déterministe / non reproductible** (temps, aléa, asynchrone).

## Reasons for the Problem

**Pourquoi cela arrive**

* **Timing implicite.** Le code asynchrone est « synchronisé » par des `sleep`s fixes ou en n'attendant pas du tout. Le délai est une supposition : assez long aujourd'hui, trop court sous charge demain.
* **Couplage caché.** Les tests partagent une base de données, un singleton, des variables de niveau module ou des fichiers sur disque. Les effets de bord d'un test deviennent les préconditions d'un autre (_Tests interagissants_ / _Guerre des exécutions de tests_ de Meszaros), si bien que les résultats dépendent de l'ordre et de ce qui s'est exécuté en parallèle.
* **Des entrées non déterministes s'infiltrent.** L'heure réelle de l'horloge murale, `Math.random()`, la locale/le fuseau horaire et les collections non ordonnées varient d'une exécution et d'une machine à l'autre.
* **Dépendance optimiste à l'environnement.** Les tests sollicitent un réseau/service en direct ou supposent qu'un fichier existe (_Optimisme sur les ressources_) et ne libèrent jamais ce qu'ils acquièrent (_Fuite de ressources_).
* **Assertions qui peuvent être sautées.** Mettre `expect` dans une conditionnelle ou un `.catch` signifie que la vérification peut ne jamais s'exécuter, si bien que le test « passe » par accident.

**Pourquoi c'est nuisible**

* **Fausse confiance et défauts masqués.** Un test instable peut échouer pour des raisons sans rapport avec la production, _et_ une vraie régression peut se cacher derrière un échec que tout le monde suppose « juste de l'instabilité ». On ne distingue plus le signal du bruit.
* **Érosion de la confiance.** Une fois qu'une suite est connue pour être instable, les développeurs ignorent les builds rouges et relancent par réflexe, ce qui entraîne l'équipe à ignorer entièrement la suite de tests.
* **Temps perdu et pipelines cassés.** Les nouvelles tentatives, relances et enquêtes « est-ce moi ou le test ? » ralentissent tout le monde et bloquent le CI/CD sur des non-problèmes.
* **Mauvaise maintenabilité.** Les tests erratiques sont difficiles à déboguer parce que l'échec n'est pas reproductible ; ils tendent à être désactivés plutôt que corrigés, réduisant discrètement la couverture réelle.

## Treatment

Traitez l'instabilité comme un défaut du test, pas comme une bizarrerie à contourner par des relances. Les relances servent à **détecter** l'instabilité, jamais à la **masquer**. Mettez le test en quarantaine s'il bloque le pipeline, puis remontez à sa cause racine.

**1\. Remplacez les sleeps par une attente basée sur une condition.** Sondez l'état qui vous intéresse réellement au lieu de deviner une durée.

```js
// avant — délai fixe et instable
fireEvent.click(screen.getByText('Pay'));
await new Promise(r => setTimeout(r, 300));
expect(screen.getByRole('status')).toBeInTheDocument();

// après — attendre la condition, puis affirmer
fireEvent.click(screen.getByText('Pay'));
expect(await screen.findByRole('status')).toBeInTheDocument();

```

**2\. Rendez les entrées déterministes.** Injectez l'horloge ou utilisez de faux timers ; semez ou simulez l'aléa ; figez la locale/le fuseau horaire.

```js
// avant : dépend de la date réelle
expect(label).toBe(`Paid ${new Date().toISOString()}`);

// après : figer le temps (vitest/jest)
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-15T00:00:00Z'));

```

**3\. Isolez chaque test.** Donnez à chaque test des fixtures fraîches, réinitialisez l'état partagé (BD, singletons, globales de module) dans `beforeEach`/`afterEach`, et évitez les variables mutables de niveau module. Exécutez la suite dans un **ordre aléatoire** (par ex. `--seed`/randomize de Jest, `MethodOrderer.Random` de JUnit) pour faire émerger tôt les dépendances à l'ordre.

**4\. Simulez les ressources externes.** Mockez le réseau, le système de fichiers et les appels système pour que le test ne dépende jamais d'un service en ligne (soigne l'_Optimisme sur les ressources_) ; libérez/nettoyez toujours les ressources acquises (soigne la _Fuite de ressources_).

**5\. Attendez tout le travail asynchrone et affirmez sans condition.** Renvoyez/attendez les promesses et les utilitaires de test asynchrones ; sortez les effets de bord des callbacks `waitFor` pour qu'ils s'exécutent une seule fois ; n'enterrez jamais `expect` dans un `if`/`catch`.

**6\. Vérifiez le correctif.** Exécutez le test désormais déterministe de nombreuses fois (et sous charge / dans un ordre mélangé) pour confirmer sa stabilité avant de le sortir de quarantaine.

## Detected by

- **sonar** `java:S5973` — Les tests doivent être stables (https://rules.sonarsource.com/java/RSPEC-5973/)
- **sonar** `java:S2925` — « Thread.sleep » ne doit pas être utilisé dans les tests (https://rules.sonarsource.com/java/RSPEC-2925/)
- **eslint-jest** `no-conditional-expect` — no-conditional-expect (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-vitest** `no-conditional-expect` — no-conditional-expect (https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-testing-library** `await-async-queries` — await-async-queries (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md)
- **eslint-testing-library** `await-async-utils` — await-async-utils (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md)
- **eslint-testing-library** `no-wait-for-side-effects` — no-wait-for-side-effects (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md)
