Outils du métier

Les tests automatisés sont essentiellement du code qui génère ou provoque une erreur en cas de problème. La plupart des bibliothèques ou des frameworks de test fournissent diverses primitives qui facilitent l'écriture des tests.

Comme indiqué dans la section précédente, ces primitives permettent presque toujours de définir des tests indépendants (appelés cas de test) et de fournir des assertions. Les assertions sont un moyen de combiner la vérification d'un résultat et la génération d'une erreur en cas de problème. Elles peuvent être considérées comme la primitive de base de toutes les primitives de test.

Cette page présente une approche générale de ces primitives. Le framework que vous avez choisi contient probablement quelque chose comme celui-ci, mais il ne s'agit pas d'une référence exacte.

Exemple :

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

Cet exemple crée un groupe de tests (parfois appelé suite) appelé "tests de mathématiques" et définit trois scénarios de test indépendants qui exécutent chacun des assertions. Ces scénarios de test peuvent généralement être traités ou exécutés individuellement, par exemple, par un indicateur de filtre dans votre lanceur de test.

Assistants d'assertion en tant que primitives

La plupart des frameworks de test, y compris Vitest, incluent une collection d'aides à l'assertion sur un objet assert qui vous permettent de vérifier rapidement les valeurs renvoyées ou d'autres états par rapport à certaines expectation. Cette attente correspond souvent à des valeurs "bien connues". Dans l'exemple précédent, nous savons que le 13e nombre de Fibonacci devrait être 233. Nous pouvons donc le confirmer directement en utilisant assert.equal.

Vous pouvez également vous attendre à ce qu'une valeur prenne une certaine forme, qu'elle soit supérieure à une autre valeur ou qu'elle possède une autre propriété. Ce cours ne couvre pas la gamme complète des assistants d'assertion possibles, mais les frameworks de test fournissent toujours au moins les vérifications de base suivantes:

  • Une vérification 'truthy', souvent décrite comme une vérification "OK", permet de vérifier qu'une condition est vraie, ce qui correspond à la façon dont vous pouvez écrire une if qui vérifie si quelque chose est correct ou correct. Il est généralement fourni sous la forme assert(...) ou assert.ok(...), et utilise une seule valeur plus un commentaire facultatif.

  • Contrôle d'égalité, comme dans l'exemple de test mathématique, dans lequel l'état ou la valeur renvoyée d'un objet doivent correspondre à une bonne valeur connue. Elles servent à l'égalité primitive (par exemple, pour les nombres et les chaînes) ou à l'égalité référentielle (il s'agit du même objet). En arrière-plan, il s'agit simplement d'une vérification de l'intégrité avec une comparaison == ou ===.

    • JavaScript fait la distinction entre une égalité faible (==) et une égalité stricte (===). La plupart des bibliothèques de test fournissent respectivement les méthodes assert.equal et assert.strictEqual.
  • Des contrôles d'égalité approfondis, qui étendent les contrôles d'égalité pour inclure la vérification du contenu des objets, des tableaux et d'autres types de données plus complexes, ainsi que la logique interne permettant de balayer les objets pour les comparer. Ils sont importants, car JavaScript ne dispose d'aucun moyen intégré pour comparer le contenu de deux objets ou tableaux. Par exemple, [1,2,3] == [1,2,3] est toujours "false". Les frameworks de test incluent souvent des assistants deepEqual ou deepStrictEqual.

Les assistants d'assertion qui comparent deux valeurs (plutôt qu'une vérification de "vérité" uniquement) prennent généralement deux ou trois arguments:

  • Valeur réelle, telle que générée à partir du code testé ou décrivant l'état à valider.
  • Valeur attendue, généralement codée en dur (par exemple, un nombre littéral ou une chaîne)
  • Commentaire facultatif décrivant ce qui était attendu ou ce qui aurait pu avoir échoué, qui sera inclus en cas d'échec de cette ligne.

Il est également assez courant de combiner des assertions pour créer diverses vérifications, car il est rare qu'une personne puisse confirmer correctement l'état de votre système par elle-même. Exemple :

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest utilise la bibliothèque d'assertions Chai en interne pour fournir ses assistants d'assertion. Il peut être utile d'examiner sa référence pour déterminer quelles assertions et quels assistants pourraient convenir à votre code.

Assertions fluides et BDD

Certains développeurs préfèrent un style d'assertion qui peut être appelé développement basé sur le comportement (BDD) ou assertion de style Fluent. Ils sont également appelés "helpers attendus", car le point d'entrée pour vérifier les attentes est une méthode nommée expect().

Les assistants se comportent de la même manière que les assertions écrites sous forme d'appels de méthode simples tels que assert.ok ou assert.strictDeepEquals, mais certains développeurs les trouvent plus faciles à lire. Une assertion BDD peut se présenter comme suit:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Ce style d'assertion fonctionne grâce à une technique appelée "chaînage de méthodes", où l'objet renvoyé par expect peut être enchaîné en continu avec des appels de méthode supplémentaires. Certaines parties de l'appel, y compris to.be et that.does dans l'exemple précédent, n'ont pas de fonction et ne sont incluses que pour faciliter la lecture de l'appel et potentiellement pour générer un commentaire automatisé en cas d'échec du test. (Notez que expect ne prend normalement pas en charge les commentaires facultatifs, car le chaînage doit décrire clairement l'échec.)

De nombreux frameworks de test sont compatibles à la fois avec Fluent/BDD et avec les assertions standards. Vitest, par exemple, exporte les deux approches de Chai et a sa propre approche légèrement plus concise pour BDD. Jest, en revanche, n'inclut qu'une méthode attendue par défaut.

Regrouper les tests sur plusieurs fichiers

Lors de l'écriture de tests, nous avons déjà tendance à fournir des regroupements implicites. Plutôt que que tous les tests se trouvent dans un seul fichier, il est courant d'écrire des tests dans plusieurs fichiers. En fait, les exécuteurs de test ne savent généralement qu'un fichier est destiné au test en raison d'un filtre ou d'une expression régulière prédéfinis. Par exemple, "vitest" inclut tous les fichiers de votre projet qui se terminent par une extension telle que ".test.jsx" ou ".spec.ts" (".test" et ".spec", ainsi qu'un certain nombre d'extensions valides).

Les tests de composants ont tendance à se trouver dans un fichier appairé au composant testé, comme dans la structure de répertoire suivante:

Liste des fichiers d'un répertoire, y compris UserList.tsx et UserList.test.tsx.
Un fichier de composant et un fichier de test associé.

De même, les tests unitaires ont tendance à être placés à côté du code testé. Les tests de bout en bout peuvent se trouver dans leur propre fichier, et les tests d'intégration peuvent même être placés dans leurs propres dossiers uniques. Ces structures peuvent être utiles lorsque des scénarios de test complexes s'agrandissent et nécessitent l'utilisation de leurs propres fichiers d'assistance autres que des fichiers de test, tels que les bibliothèques d'aide nécessaires uniquement à un test.

Regrouper les tests dans des fichiers

Comme dans les exemples précédents, il est courant de placer les tests dans un appel à suite() qui regroupe les tests que vous avez configurés avec test(). Les suites ne sont généralement pas des tests eux-mêmes, mais elles contribuent à fournir une structure en regroupant les tests ou les objectifs associés en appelant la méthode transmise. Pour test(), la méthode transmise décrit les actions du test lui-même.

Comme pour les assertions, il existe une équivalence assez standard entre Fluent/BDD et les tests de regroupement. Quelques exemples typiques sont comparés dans le code suivant:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

Dans la plupart des frameworks, suite et describe se comportent de la même manière que test et it, par opposition aux plus grandes différences entre l'utilisation de expect et assert pour écrire des assertions.

D'autres outils utilisent des approches subtilement différentes pour organiser les suites et les tests. Par exemple, le lanceur de test intégré de Node.js accepte l'imbrication d'appels à test() pour créer implicitement une hiérarchie de test. Cependant, Vitest n'autorise ce type d'imbrication qu'à l'aide de suite() et n'exécute pas de test() défini dans un autre test().

Comme pour les assertions, n'oubliez pas que la combinaison exacte des méthodes de regroupement fournies par votre pile technologique n'est pas très importante. Ce cours les traite de manière abstraite, mais vous devez déterminer comment ils s'appliquent à votre choix d'outils.

Méthodes du cycle de vie

L'une des raisons de regrouper vos tests, même implicitement au niveau supérieur d'un fichier, est de fournir des méthodes de configuration et de suppression qui s'exécutent pour chaque test ou une fois pour un groupe de tests. La plupart des cadres proposent quatre méthodes:

Pour chaque test() ou it() Une fois pour la suite
Avant les exécutions des tests "beforeChaque()" "beforeAll()"
Après le test "afterChaque()" "afterAll()"

Par exemple, vous pouvez pré-remplir une base de données d'utilisateurs virtuels avant chaque test et l'effacer par la suite:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Cela peut être utile pour simplifier vos tests. Vous pouvez partager des codes de configuration et de suppression communs, plutôt que de les dupliquer dans chaque test. De plus, si le code de configuration et de suppression génère lui-même une erreur, cela peut indiquer des problèmes structurels qui n'entraînent pas l'échec des tests.

Conseils d'ordre général

Voici quelques conseils à retenir lorsque vous pensez à ces primitives.

Les primitives servent de guide

N'oubliez pas que les outils et les primitives présentés ici, ainsi que dans les pages suivantes, ne correspondront pas exactement à Vitest, Jest, Mocha, Web Test Runner ni à tout autre framework spécifique. Nous avons utilisé Vitest comme guide général, mais veillez à les associer à votre choix de framework.

Combinez les assertions si nécessaire

Les tests sont essentiellement du code qui peut générer des erreurs. Chaque exécuteur fournit un test() primitif, vraisemblablement, pour décrire des scénarios de test distincts.

Toutefois, si cet exécuteur fournit également assert(), expect() et des assistants d'assertion, n'oubliez pas que cette partie est plus pratique et que vous pouvez l'ignorer si nécessaire. Vous pouvez exécuter tout code susceptible de générer une erreur, y compris d'autres bibliothèques d'assertion ou une instruction if à l'ancienne.

La configuration de l'IDE peut vous sauver la vie

Vous pouvez améliorer votre productivité en vous assurant que votre IDE, comme VSCode, a accès à la saisie semi-automatique et à la documentation sur les outils de test que vous avez choisis. Par exemple, la bibliothèque d'assertions Chai contient plus de 100 méthodes sur assert. Il peut donc être pratique d'intégrer la documentation de la méthode appropriée.

Cela peut être particulièrement important pour certains frameworks de test qui renseignent l'espace de noms global avec leurs méthodes de test. Il s'agit d'une légère différence, mais il est souvent possible d'utiliser des bibliothèques de test sans les importer si elles sont automatiquement ajoutées à l'espace de noms global:

// some.test.js
test('using test as a global', () => { … });

Nous vous recommandons d'importer les assistants même s'ils sont pris en charge automatiquement, car cela donne à votre IDE un moyen clair de rechercher ces méthodes. (Vous avez peut-être rencontré ce problème lors de la création de React, car certains codebases ont un React global magique, mais d'autres non, et exigent qu'il soit importé dans tous les fichiers à l'aide de React.)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });