Écrivez des tests unitaires

Dans le chapitre précédent, vous avez préparé votre environnement de test et vérifié que Vitest fonctionnait correctement grâce à un premier test très simple. Votre tech lead était satisfait : l'outillage est en place et prêt à être utilisé. Mais tester que1 + 1 = 2ne protège pas encore votre application de gestion de tâches. Les vrais enjeux commencent maintenant.

L'équipe travaille actuellement sur le service d'authentification. Avant d'ajouter de nouvelles fonctionnalités, votre tech lead souhaite sécuriser un composant essentiel : le hachage des mots de passe.

Pourquoi commencer par cette fonction ? 

Parce qu'elle constitue un excellent candidat pour découvrir les tests unitaires. Elle est simple, isolée et ne dépend ni d'une base de données, ni d'une API, ni d'une interface utilisateur.

Vous allez donc écrire vos premiers vrais tests sur une fonctionnalité métier du projet. Au passage, vous découvrirez une méthode de structuration universelle, apprendrez à couvrir des cas limites et développerez un réflexe indispensable : savoir lire un message d'échec avant de modifier votre code.

Comprenez ce que teste un test unitaire

Dans le dernier chapitre, vous avez découvert les quatre niveaux de test présents dans une application full-stack : test unitaire, intégration back-end, test de composant et test end-to-end.

Aujourd'hui, nous allons nous concentrer sur le premier niveau : le test unitaire. Un test unitaire vérifie le comportement d'une seule fonction, isolée de tout le reste de l'application. Le mot important est isolée. Concrètement, cela signifie que la fonction testée ne doit pas dépendre :

  • d'une base de données ;

  • d'un appel réseau ;

  • d'un service externe ;

  • d'un composant React ;

  • d'un effet de bord imprévisible.

Pourquoi cette contrainte ? 

Imaginez que vous souhaitiez vérifier qu'une calculatrice additionne correctement deux nombres. Si, pour réaliser ce test, vous devez également démarrer un serveur, connecter une base de données et charger une interface graphique, il devient difficile de savoir d'où provient une éventuelle erreur.

Le test unitaire cherche au contraire à répondre à une question très précise :

Cette fonction produit-elle le résultat attendu lorsque je lui fournis une entrée donnée ?

Pour structurer nos tests, nous allons utiliser un modèle très répandu : AAA, pour Arrange (Préparer), Act (Agir), Assert (Vérifier). Cette structure permet de lire rapidement l'intention d'un test.

Schéma en trois étapes : Arrange pour préparer les données, Act pour exécuter l’action, puis Assert pour vérifier le résultat.
La méthode AAA

Arrange : Préparez les données nécessaires au scénario.

Par exemple :

const password = "MonMotDePasse123";

Act : Il est temps d’agir. Exécutez l'action à tester. Comme

hashPassword()est asynchrone, on l'attend avecawait.

const result = await hashPassword(password);

Assert : Vérifiez que le résultat correspond à ce que vous attendez.

expect(result).not.toBe(password);

Écrivez votre premier test unitaire

Vous êtes maintenant prêt à tester une vraie fonctionnalité de l'application. Votre tech lead vous demande de sécuriser la fonctionhashPassword()située dans le fichierlib/auth/hash.ts.

Avant de vous lancer dans l’écriture du test, observez dans ce screencast comment créer le fichier de test, appliquer la structure AAA et lire les messages de succès ou d’échec affichés par Vitest.

À vous maintenant de reproduire la démarche sur la fonctionhashPassword().

Commençons par créer le fichier :

lib/auth/hash.test.ts

Nous importons ensuite la fonction à tester :

import { hashPassword } from "./hash";

Puis les utilitaires fournis par Vitest :

import { describe, it, expect } from "vitest";

Nous pouvons maintenant écrire notre premier test.

describe("hashPassword", () => {
  it("should return a hashed string when given a valid password", async () => {

    // Arrange
    const password = "MonMotDePasse123";

    // Act
    const hashedPassword = await hashPassword(password);

    // Assert
    expect(hashedPassword).not.toBe(password);
    expect(typeof hashedPassword).toBe("string");

  });
});

Prenez quelques secondes pour relire ce test.

Remarquez comme la structure AAA apparaît naturellement :

  • préparation de la donnée ;

  • exécution de la fonction ;

  • vérification du résultat.

Lançons maintenant Vitest :

npm run test

Si tout est correctement configuré, le terminal affiche un résultat vert. Votre premier test métier est passé. Mais attention : un développeur débutant découvre rarement uniquement des tests verts. C'est pourquoi votre tech lead vous montre volontairement un test erroné :

expect(hashedPassword).toBe(password);

Cette assertion est fausse puisque le mot de passe haché ne doit jamais être identique au mot de passe d'origine. Vitest affiche alors un échec, et c'est une excellente nouvelle. Un test qui échoue permet de vérifier que votre système de validation détecte réellement les problèmes. Un ensemble de tests qui ne peut jamais échouer n'apporte aucune sécurité.

Gérez les cas limites et les erreurs

À ce stade, votre couverture de test reste encore incomplète. Pourquoi ? Parce que nous avons uniquement vérifié le scénario idéal. Dans la vraie vie, les utilisateurs ne suivent pas toujours le chemin prévu. Certains saisissent des mots de passe vides. D'autres utilisent des chaînes très longues. Certaines données peuvent même être incorrectes. C'est là qu'interviennent les cas limites.

Un cas limite représente une situation particulière susceptible de révéler un comportement inattendu. Pour notre fonction de hachage, nous pouvons envisager plusieurs scénarios :

  • un mot de passe valide ;

  • une chaîne vide ;

  • une chaîne extrêmement longue.

Notez quehashPassword()est typée pour ne recevoir qu'unestring: TypeScript empêche déjà de lui passer une valeur d'un autre type. Par ailleurs, bcrypt tronque l'entrée à 72 octets, si bien que tester une très longue chaîne vérifie surtout que la fonction reste robuste et renvoie bien une chaîne.

Pour organiser ces scénarios, nous utilisons plusieursit()dans un mêmedescribe().

describe("hashPassword", () => {

  it("should return a hashed string when given a valid password", async () => {
    // ...
  });

  it("should handle an empty string", async () => {
    // ...
  });

  it("should hash a very long password", async () => {
    // ...
  });

});

Vous remarquerez également l'importance du nommage.

Comparez ces deux exemples :

it("test 1", () => {});
it("should return a hashed string when given a valid password", () => {});

Lequel est le plus explicite ? Le second, évidemment. Le nom du test doit permettre à n'importe quel membre de l'équipe de comprendre immédiatement le comportement attendu. C'est particulièrement utile lorsqu'un projet contient plusieurs centaines de tests. Un bon nom agit comme une documentation vivante. Avant d'écrire un nouveau test, posez-vous toujours cette question :

Si ce test échoue dans six mois, est-ce que son nom me permettra de comprendre immédiatement le problème ?

Si la réponse est non, renommez-le.

Lisez et interprétez un message d'échec

Lorsqu'un test échoue, le premier réflexe de nombreux développeurs juniors consiste à modifier immédiatement le code. Pourtant, cette réaction est souvent contre-productive. Le bon réflexe consiste d'abord à lire le message d'erreur.

Supposons que vous obteniez le résultat suivant :

FAIL hash.test.ts

Expected: "password123"
Received: "$2b$10$G8L..."

AssertionError: expected '$2b$10$G8L...' to be 'password123'

Comment interpréter ce message ? Commencez par identifier la comparaison effectuée.

Vitest vous indique :

  • la valeur attendue ;

  • la valeur réellement obtenue.

Ici, le test attendait le mot de passe original :

Expected: "password123"

Mais la fonction a renvoyé une chaîne hachée :

Received: "$2b$10$G8L..."

L'erreur ne provient donc pas nécessairement de la fonction. Elle peut simplement provenir d'une assertion incorrecte. Ensuite, regardez la ligne signalée par Vitest. Elle pointe généralement vers l'instructionexpect()responsable de l'échec. Cette information permet souvent d'identifier la cause du problème en quelques secondes. Votre objectif n'est pas de faire disparaître le message d'erreur. Votre objectif est de comprendre ce qu'il vous dit.

À vous de jouer !

Contexte

Après avoir validé l'installation de Vitest dans le chapitre précédent, votre tech lead vous confie une nouvelle mission. L'équipe souhaite renforcer la qualité du service d'authentification de l'application de gestion de tâches. La fonctionhashPassword()est déjà implémentée dans le projet, mais aucun test ne garantit encore son comportement.

Vous devez donc créer la première couverture de test de ce service.

Consignes

  1. Créez le fichierlib/auth/hash.test.ts.

  2. Écrivez un test couvrant le cas nominal.

  3. Ajoutez deux tests supplémentaires couvrant des cas limites.

  4. Donnez un nom explicite à chaque test.

  5. Exécutez Vitest et vérifiez que tous les tests passent.

  6. Modifiez volontairement une assertion afin de provoquer un échec.

  7. Analysez le message affiché par Vitest avant de corriger l'erreur.

En résumé

  • Un test unitaire vérifie une fonction isolée, sans base de données, réseau ou autre dépendance externe.

  • La structure AAA (Arrange, Act, Assert) facilite la lecture et l'organisation des tests.

  • Un blocdescribe()peut regrouper plusieursit()afin de couvrir le cas nominal et différents cas limites.

  • Un nom de test explicite permet de comprendre immédiatement le comportement attendu.

  • Lire attentivement un message d'échec Vitest est le premier réflexe à adopter avant toute modification du code.

Vous savez maintenant écrire et organiser des tests unitaires. Dans le prochain chapitre, vous apprendrez à tester des routes d'API afin de vérifier le comportement complet de la couche back-end de l'application de gestion de tâches.

Ever considered an OpenClassrooms diploma?
  • Up to 100% of your training program funded
  • Flexible start date
  • Career-focused projects
  • Individual mentoring
Find the training program and funding option that suits you best