ConstructiCat Logo
CodeBust.
Browse section ▾

Assertion Roulette.

Un test entasse de nombreuses assertions non documentées dans une seule méthode : lorsqu'il échoue, impossible de savoir quelle assertion s'est déclenchée ni pourquoi — vous devez jouer à la roulette.

##Signs and Symptoms

Un seul test contient une succession d'assertions nues, sans message explicatif et sans objectif unique clair. Lorsqu'il passe au rouge, le rapport d'échec (en particulier une ligne de synthèse de CI ou un assistant d'assertion partagé) vous indique qu'une chose a cassé, mais pas laquelle ni pourquoi — vous jouez à la « roulette » pour trouver le coupable.

Signes révélateurs :

  • De nombreux appels expect(...) / assert(...) dans un même test, aucun ne portant de message ou d'étiquette descriptive.
  • Le nom du test est générique ('works', 'user profile', 'test register') : un échec ne communique rien à lui seul.
  • Les assertions couvrent plusieurs préoccupations sans rapport (un Eager Test qui réutilise une même fixture pour tout vérifier d'un coup).
  • Un échec vous pousse à dégainer le débogueur ou les numéros de ligne pour comprendre ce qui était réellement vérifié.
  • Comme la plupart des assertions sont « fail-fast », le premier échec court-circuite le reste : vous ne voyez jamais les vérifications suivantes en une seule exécution.
test('user profile', () => {
  const user = createUser({ name: 'Ada', age: 36 });
  expect(user.name).toBe('Ada');
  expect(user.age).toBe(36);
  expect(user.isAdult).toBe(true);   // si C'EST celle-ci qui est rouge,
  expect(user.slug).toBe('ada');     // le rapport dit simplement
  expect(user.roles).toContain('member'); // "expected false to be true"
  expect(user.createdAt).toBeInstanceOf(Date);
});

Remarque : les exécuteurs modernes (Jest, Vitest) affichent la ligne défaillante et un diff, ce qui atténue le problème du « quelle ligne ? ». La mauvaise odeur mord encore lorsque le test mêle des objectifs, porte un nom vague, boucle sur des assertions, les cache derrière des assistants partagés, ou s'exécute là où seul un message de synthèse subsiste.

##Reasons for the Problem

Pourquoi cela se produit

  • Test trop gourmand (Eager Test) / réutilisation de la fixture. Une mise en place coûteuse incite à empiler des vérifications « tant qu'on y est » plutôt qu'à écrire un second test.
  • Vérification d'objet entier faite à la dure. Vérifier un objet champ par champ produit naturellement une longue série d'assertions non documentées.
  • Croissance par copier-coller. Les tests accumulent des assertions au fil du temps sans que personne ne les scinde.
  • Outillage historique. Les assertions classiques de xUnit ne signalaient que succès/échec : une assertion sans libellé n'offrait aucun contexte en cas d'échec — l'origine de la mauvaise odeur décrite à l'origine par Meszaros.
  • Pression des délais. Un gros test paraît plus rapide à écrire que plusieurs tests ciblés.

Pourquoi c'est nuisible

  • Diagnosticabilité / fiabilité. Selon testsmells.org, « la présence de plusieurs assertions dans une méthode de test sans message descriptif nuit à la lisibilité, à la compréhension et à la maintenabilité, car il est impossible de comprendre la raison de l'échec. » Vous perdez du temps à localiser l'assertion qui a déclenché l'échec.
  • Défauts masqués (fausse confiance). Les assertions « fail-fast » s'arrêtent au premier échec : les assertions suivantes ne s'exécutent jamais. Vous corrigez la première, relancez, trouvez la suivante — plusieurs bogues sont masqués, et un « vert après une seule correction » paraît plus rassurant qu'il ne l'est.
  • Lisibilité. Le test cesse de documenter un comportement unique ; son intention se perd dans une liste, et un nom de test générique n'apporte rien.
  • Maintenabilité. On ne sait pas si une nouvelle vérification a sa place dans ce test ou dans un autre, donc la pile continue de grossir et des préoccupations sans rapport finissent couplées.

##Treatment

Visez le principe qui sous-tend le Single-Condition Test : chaque test vérifie un seul comportement et ne peut échouer que pour une seule raison.

  1. Scindez par objectif. Décomposez le test fourre-tout en tests ciblés portant des noms descriptifs. Le nom devient alors le message d'échec.
  2. Regroupez les vérifications champ par champ en une seule assertion. Utilisez toEqual / expect.objectContaining / un instantané (snapshot) relu, pour que N assertions deviennent un seul diff parlant.
  3. Lorsque le regroupement est réellement cohérent, libellez les assertions. expect de Jest/Vitest n'accepte pas d'argument de message : utilisez le paramètre de message de node:assert, le expect(actual, message) de Vitest, ou jest-expect-message.
  4. Vous voulez que chaque vérification s'exécute et soit rapportée ensemble ? Préférez scinder ; sinon recourez aux assertions souples (expect.soft dans Vitest) pour qu'un échec n'en masque pas les autres.
  5. Prémunissez-vous contre les régressions en plafonnant le nombre d'assertions par test (voir les détecteurs).

Avant — assertion roulette :

test('register user', () => {
  const res = register({ email: 'a@b.com', age: 36 });
  expect(res.ok).toBe(true);
  expect(res.user.email).toBe('a@b.com');
  expect(res.user.isAdult).toBe(true);
  expect(res.welcomeEmailSent).toBe(true);
});

Après — un comportement par test, avec une assertion sur l'objet entier :

describe('register', () => {
  it('accepts a valid adult signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
  });

  it('stores the normalized user record', () => {
    const { user } = register({ email: 'a@b.com', age: 36 });
    expect(user).toEqual(
      expect.objectContaining({ email: 'a@b.com', isAdult: true }),
    );
  });

  it('sends a welcome email on signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
  });
});

Si vous devez conserver un seul test, rendez chaque assertion auto-descriptive :

import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');

##Detected by

  • eslint-jest jest/max-expectsSignale l'indicateur fondé sur le décompte qui sert d'approximation à l'Assertion Roulette : alerte lorsqu'un test dépasse N appels à expect() (5 par défaut). La documentation note que multiplier les assertions tend à mêler plusieurs objectifs.
  • eslint-vitest vitest/max-expectsÉquivalent Vitest : impose un nombre maximal d'assertions expect() par test, repérant les tests qui accumulent trop de vérifications.
  • sonar java:S5961SonarSource « Test methods should not contain too many assertions » — plafonne le nombre d'assertions par test (25 par défaut pour JUnit/AssertJ), l'approximation standard de cette mauvaise odeur par l'analyse statique.