Node

Jest

Réaliser des tests unitaires avec Jest

Pourquoi tester ?

Les tests permettent simplement de s'assurer du bon fonctionnement et de la qualité de votre code. En effet pour des projets aux fonctionnalités très simple il peut sembler évident, juste en démarrant notre application et en l'utilisant manuellement, qu'elle fonctionne. Mais au fur et à mesure de son évolution vous allez ajouter des aspects de plus en plus complexe.

Les test peuvent aussi servir à prévenir les soucis de régression. Imaginons que votre code évolue et une fonctionnalité est atteinte par une modification de votre code sans que vous ne vous en rendiez compte. Les test automatisés eux passeront sur l'entièreté de votre code et réussiront à détecter cette erreur.

Types de tests

Dans le vocabulaire professionnel vous entendez de nombreux types de tests, dont voici une liste non exhaustive:

  • Unit Tests (Tests unitaires)
    • Testent des fonctions ou des composants individuels en isolation.
    • Exemple : Vérifier que la fonction add(2, 3) retourne 5.
  • Integration Tests (Tests d'intégration)
    • Testent la combinaison de plusieurs unités ou modules pour s'assurer qu'ils fonctionnent ensemble.
    • Exemple : Vérifier que l'API d'une base de données renvoie les données correctes.
  • End-to-End Tests (Tests E2E)
    • Simulent le comportement des utilisateurs dans un environnement réel pour tester un système complet.
    • Exemple : Vérifier qu'un utilisateur peut s'inscrire sur un site web.
  • Performance Tests (Tests de performance)
    • Mesurent la rapidité et la réactivité du système.
    • Exemple : Vérifier que le chargement d'une page ne dépasse pas 3 secondes.
  • Accessibility Tests (Tests d'accessibilité)
    • S'assurent que le système est utilisable par tous, y compris les personnes en situation de handicap.
    • Exemple : Vérifier que tous les boutons ont des étiquettes ARIA appropriées.
  • UI Tests (Tests d'interface utilisateur)
    • Vérifient que l'interface utilisateur s'affiche et fonctionne comme prévu.
    • Exemple : S'assurer qu'une icône apparaît au bon endroit dans une application.

Le framework Jest

Pour réaliser des tests en NodeJS on peut utiliser le package jest et ts-jest pour les tests en typescript. Le package @types/jest est aussi nécessaire pour les tests en typescript.

Commençons par installer nos packages:

npm install -D jest ts-jest @types/jest`

Configuration

Pour commencer nous allons créer un fichier de configuration pour Jest. Créer un fichier jest.config.ts qui contiendra la configuration de Jest pour fonctionner avec TypeScript.

import type { Config } from 'jest';

const config: Config = {
  testEnvironment: 'node',
  transform: {
    '^.+.tsx?$': ['ts-jest', {}],
  },
};

export default config;

Modifier ensuite votre package.json en lui ajoutant un script supplémentaire: "test": "jest".

Vous pouvez désormais lancer la commande npm run test qui cherchera tout vos fichier de types *.spec.js ou *.test.js.

En effet ces fichiers définissent des fichiers de test, il est généralement recommandé de créer un fichier *.test.js pour chaque fichier que vous posséder. Par exemple si nous avons un fichier math.js qui assumera des fonctions mathématique, nous créerons son fichier de test correspondant math.test.js.

Premier test unitaire

Commençons avec des tests unitaires très simple. Nous allons essayer de tester une fonction sum contenue dans le fichier math.ts comme celle que nous avions définie plus tôt dans ce cours.

Créer un fichier tests/math.test.js et ajoutez y les lignes suivantes:

import { sum } from '../src/math';

describe('sum function', () => {
  test('adds 1 + 2 to equal 3', () => {
    const res = sum(1, 2);
    expect(res).toBe(3);
  });
});
  • test indique que nous allons créer un test avec son intitulé puis il prend en second paramètre un callback de ce que le test doit vérifier.
  • expect correspond à l'attendu. toBe est une fonction qui vérifie que le résultat de la fonction sum est bien égal à 3.

Lancez la commande npm run test et nous constaterons que les tests sont au vert indiquant que la fonction a bien été testé avec les paramètres donnés.

Vous trouverez la liste des fonctions de comparaison ici.

Hiérarchisation des tests

Supposons que nous voulons tester plusieurs fonctions différentes et que chaque fonction possèdera sa propre batterie de test. Il est possible de hiérarchier les tests grâce à la fonction describe.

describe('sum function', () => {
  test('adds 1 + 2 to equal 3', () => {
    const res = sum(1, 2);
    expect(res).toBe(3);
  });

  test('add -1 + 0', () => {
    const res = sum(-1, 0);
    expect(res).toBe(-1);
  });
});

Remarque : Depuis le début nous utilisons le mot clé test pour définir un test. Il est possible de le remplacer par it qui est un alias. C'est d'ailleurs souvent préféré pour des raisons de lisibilité. À partir de maintenant nous utiliserons it pour définir nos tests dans ce cours.

Coverage

Le coverage est une notion importante dans les tests. Il permet de savoir si l'ensemble de votre code est testé. Jest permet de générer un rapport de couverture de code pour savoir si l'ensemble de votre code est testé ou non sous forme de pourcentage ou alors d'un rapport détaillé ligne par ligne.

Pour ajouter le coverage dans vos test il suffit d'ajouter le suffixe --coverage à votre commande jest --coverage.

Vous trouverez le résultat du coverage dans le dossier coverage/index.html.

Seuil de couverture

Vous pouvez définir un seui de couverture pour vos tests. S'il n'est pas atteint, les tests seront considérés comme échoués. On le définit dans le fichier jest.config.js avec la clé coverageThreshold. Par exemple, pour définir un seuil de 90% pour chaque type de couverture (branches, functions, lines, statements), ajoutez le code suivant :

module.exports = {};

import type { Config } from 'jest';

const config: Config = {
  testEnvironment: 'node',
  transform: {
    '^.+.tsx?$': ['ts-jest', {}],
  },
  coverageThreshold: {
    global: {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90,
    },
  },
};

export default config;

Remarque: On appréciera généralement un seuil de couverture de 90% ou plus pour un projet en production.

Environnement de test

Notre but est maintenant de tester une API. Cependant un problème va se poser. En effet, si nous testons une API, nous allons devoir y faire appel et fatalement altérer les données de la base en les supprimant ou en les modifiant. Pour éviter cela il y a plusieurs méthodes:

  • Utiliser une base de données de test.
  • Utiliser un mock de la base de données, c'est-à-dire changer les retours des fonctions qui font appel à la base de données.
  • Utiliser une base de données en mémoire, qui sera supprimée à la fin des tests.

Chacune de ces solutions est viable et peut dépendre d'un contexte. Comme nous voulons une solution simple et rapide nous allons préférer mocker la base de données.

Mocking de prisma

Pour mocker la base de données nous allons utiliser le package jest-mock-extended. Ce package permet de mocker des fonctions et de les remplacer par des fonctions vides ou des fonctions qui renvoient des valeurs spécifiques.

Il faut préciser avant le démarrage des tests qu'il faut remplacer notre singleton 'client.ts' par le mock. Cela peut se faire à travers un fichier de setup tests/jest.setup.ts qui sera appelé avant le lancement des tests.

import { PrismaClient } from '@prisma/client';
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';
import prisma from '../src/client';

jest.mock('../src/client', () => ({
  __esModule: true,
  default: mockDeep<PrismaClient>(),
}));

beforeEach(() => {
  mockReset(prismaMock);
});

export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>;

Il faut aussi modifier le fichier jest.config.ts pour lui indiquer le fichier de setup.

import type { Config } from 'jest';

const config: Config = {
  testEnvironment: 'node',
  transform: {
    '^.+.tsx?$': ['ts-jest', {}],
  },
  coverageThreshold: {
    global: {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90,
    },
  },
  setupFilesAfterEnv: ['./tests/jest.setup.ts'],
};

export default config;

Création de tests

Maintenant nous allons créer un test pour une route de notre API. Commençons par tester la route /users qui retourne tous les utilisateurs de notre base de données. Nous utilisons le package supertest pour tester notre API. Il permet de faire des requêtes HTTP sur notre serveur depuis les tests.

On ajoutera aussi le mock de prisma dans notre fichier de test. Et on modifie le retour de la fonction findMany grâce à la fonction mockResolvedValue.

import request from 'supertest';
import { app, stopServer } from '../src';
import { prismaMock } from './jest.setup';
import jwt from 'jsonwebtoken';

afterAll(() => {
  stopServer();
});

describe('GET /users', () => {
  it('should return an array of users', async () => {
    prismaMock.user.findMany.mockResolvedValue([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);

    const response = await request(app).get('/users');

    expect(response.status).toBe(200);
    expect(response.body).toEqual([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  });
});