Tests unitaires avec Jest

Magazine
Marque
GNU/Linux Magazine
Numéro
258
Mois de parution
juillet 2022
Spécialité(s)


Résumé

Que ce soit pour tester un script JavaScript, un back-end Node.js, un front-end Angular, React ou autre, en local ou sur un serveur CI/CD, Jest, le moteur de test unitaire pour JavaScript et TypeScript, va vous simplifier la vie grâce à ses nombreuses extensions et à sa facilité d’utilisation.


Body

De nos jours, un projet JavaScript ou TypeScript est généralement assez complexe pour que l’on ait à y incorporer des tests unitaires, pour des raisons de qualité de logiciel, de normes et de maintenance. Que ce soit une bibliothèque, un service, un front-end, le JavaScript et le TypeScript ne sont plus utilisés comme de simples langages scripts, mais comme des langages de développement à part entière, au même titre que les autres langages de programmation. Le temps où l’on écrivait un petit bout de code JavaScript en en-tête HTML pour animer un rectangle sur la page web qui faisait planter le navigateur est révolu. Aujourd’hui, quel que soit le langage, on utilise des frameworks : frameworks graphiques, framework d’architecture logicielle, frameworks de test...

En JavaScript, des frameworks comme Angular, Vue ou React vont gérer les templates HTML, l’authentification et les interactions utilisateur, et des frameworks supplémentaires vont générer la partie graphique, comme Angular Material ou PrimeNg pour Angular. De même, coté serveur, on peut utiliser le langage JavaScript ou TypeScript pour réaliser des webservices Rest, OData ou GraphQL grâce à Node.js, et même lui ajouter d’autres frameworks comme Express.js et NestJS. On peut aussi réaliser des applications en JavaScript et TypeScript connectées à des services SaaS (Software as a Service) situés sur le cloud, comme Google Firebase, Microsoft Azure, Amazon AWS ou MongoDB Atlas.

Aussi, la meilleure façon de s’assurer que le système fonctionne bien, avec toutes ses briques logicielles intégrées, que les données remontent bien à la page web, que la page web les affiche correctement, quelles que soient les saisies de l’utilisateur et les changements de code, c’est de tester. Pour éviter de refaire manuellement les mêmes tests, on utilise des frameworks de tests qui vont les rejouer automatiquement. Tests unitaires, tests d’intégration, tests d’interfaces graphiques, tests fonctionnels, tests de charge, tests de sécurité… il y a des tests pour chaque phase du projet, mais les premiers tests que l’on devrait mettre en place, ce sont les tests unitaires, comme indiqué par la plupart des cycles de développement logiciel [1].

Les tests logiciels sont un vaste sujet à part entière. Dans la suite de cet article, nous allons voir plus en détail les tests unitaires en JavaScript/TypeScript avec Jest.

1. Node.js

Commençons par le plus simple, le test d’un script JavaScript. Pour le tester avec Jest, il faudra le tester localement avec la plateforme Node.js.

Vous pouvez télécharger Node.js depuis le site officiel ou depuis les dépôts de votre distribution GNU/Linux. Node.js vous permettra d’exécuter des scripts JavaScript et TypeScript en local, sans passer par un navigateur. De plus, Node.js est portable (GNU/Linux, Windows, macOS) et open source, maintenu par l’OpenJS Foundation. Une fois Node.js installé, sous un terminal, vous devriez pouvoir lister la version avec la commande node -v.

$ node -v
$ v16.13.2

Node.js est livré avec un utilitaire en ligne de commande, npm (Node Package Manager), qui fournit toutes les commandes nécessaires pour télécharger les bibliothèques (packages ou modules) JavaScript ou TypeScript du dépôt (registry) officiel ou d’un dépôt personnel. Npm permet aussi de résoudre les dépendances, de construire et d’exécuter un projet. Depuis la version 5.2 de npm, Node.js comprend également un utilitaire en ligne de commande npx (Node Package Execute) pour lancer directement les commandes d’une bibliothèque.

Node.js peut également se mettre en écoute d’un port TCP et fonctionner comme un webservice, en back-end (utilisez de préférence des frameworks supplémentaires comme Express.js ou NestJS dans ce cas).

Le dépôt Node.js contient plus d’un million de packages que vous pourrez utiliser dans votre projet (attention à n’utiliser que des packages stables, reconnus, et maintenus pour la prod). De fait, les langages JavaScript/TypeScript (TypeScript étant une couche d’abstraction typée sur du JavaScript) et leurs frameworks sont devenus très populaires, autant pour le front-end que pour le back-end, comme l’indique le rapport Stack Overflow des tendances 2021 [2].

À présent que Node.js est installé, on va pouvoir créer un premier projet de test, en JavaScript, avec la commande npm init -y [3].

$ mkdir test$ cd test$ npm init -y
Wrote to test\package.json:
 
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Vous trouverez dans le répertoire test un fichier package.json généré, c’est le fichier principal de configuration pour Node.js. Ce fichier package.json contient, entre autres, le numéro de version sémantique, les commandes « scripts » particulières que l’on peut exécuter sur ce projet avec npm (npm run test, par exemple) et les packages à installer (pour l’instant non définis).

Pour info, si vous clonez un projet Node.js depuis le Web, avec Git par exemple, il ne devrait pas contenir ses bibliothèques, aussi appelées modules, généralement situées dans un répertoire node_modules, car en principe un projet Git ne contient que le code source, pour des raisons d’encombrement et d’optimisation. Vous pouvez réinstaller les bibliothèques du projet, listées dans le fichier package.json, avec la commande npm install. De même, il ne faut pas ajouter le répertoire node_modules et son contenu dans un dépôt Git, mais l’indiquer dans un fichier .gitignore.

2. Jest

Il ne reste plus qu’à installer Jest avec la commande npm install jest --save-dev. On ajoute l’option --save-dev (ou -D), car Node.js distingue deux sortes de dépendances, les dépendances de production (dependencies) et les dépendances de développement (devDependencies) qui ne concernent que les outils de développement, pour éviter la surcharge du build de production. Les packages de test, d’analyse et de mise en forme de code sont plutôt à mettre dans les dépendances de développement.

$ npm i jest --save-dev
 
added 332 packages, and audited 333 packages in 31s
 
28 packages are looking for funding
  run `npm fund` for details
 
found 0 vulnerabilities

Vous devriez voir apparaître un dossier node_modules contenant les dépendances, et le package jest listé dans les devDependencies du fichier package.json. Si vous êtes sous Git, c’est le moment d’ajouter le dossier node_modules dans un fichier .gitignore. Remarquez également que si les packages sont mis à disposition gratuitement, il est toujours possible d’aider les auteurs avec un petit don, npm fund affiche la liste des dons pour ce projet.

Pour avoir l’autocomplétion Jest, vous aurez aussi besoin du package @types/jest : npm install @types/jest --save-dev.

Dans la partie test des scripts du package.json, vous pouvez à présent indiquer jest comme application de test ; vous pourrez ainsi lancer les tests avec la commande npm run test. Votre fichier package.json devrait à présent ressembler à :

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "^27.4.1",
    "jest": "^27.5.1"
  }
}

Le chapeau ^ devant la version signifie que le projet nécessite une version compatible, ici à la version 27.5.1 de Jest. On peut aussi trouver le tilde ~ pour indiquer une version approximativement équivalente, >= pour une version supérieure ou égale, mais aussi des intervalles de versions, ou sans caractère pour une version exacte. Si une dépendance est partagée par plusieurs packages – ce qui arrive fréquemment – il faut qu’elle soit compatible avec leurs versions, sinon il faudra résoudre ces conflits de dépendances. Aussi, il est important de mettre à jour régulièrement les packages en version stable, pour éviter qu’il y ait trop de conflits lors d’un ajout, lors d’un upgrade ou lors de la maintenance du projet. Les mises à jour réduiront également les risques de failles de sécurité [4].

3. Premier test

Par défaut, Jest va tester, entre autres, tous les fichiers se terminant par test.js ou spec.js, ou les fichiers d’un répertoire __tests__. En général, on utilise plutôt les extensions spec.js (ou spec.ts). On peut bien évidemment configurer Jest, mais dans cet article, pour simplifier, on utilisera la configuration par défaut [5].

3.1 Authentication.js

Avant d’écrire un fichier spec de tests unitaires, il nous faut d’abord un fichier source à tester. Rien de mieux qu’une bonne vieille méthode d’authentification pour un premier test. En voici un exemple, authentication.js :

"use strict";
const https = require('https')
 
const authenticate = (username, password) => new Promise((resolve, reject) => {
 
    const data = JSON.stringify({
        username: username,
        password: password
      })
 
    const options = {
        hostname: 'localhost',
        port: 443,
        path: '/auth',
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Content-Length': data.length
          }
      }
 
      const req = https.request(options, res => {
        let body = [];
        res.on('data', chunk => body.push(chunk))
        res.on('end', () => {
            body = JSON.parse(Buffer.concat(body).toString())
            resolve(body)
        })
      })
      
      req.on('error', error => {
        reject(error)
      })
      
      req.write(data)
      req.end()
})
 
module.exports = {
    authenticate
}

Ce script simplifié utilise le module HTTPS interne de Node.js, mais on aurait pu utiliser des bibliothèques comme Axios, jQuery, ou Needle, mais c’est le principe qui compte (on utilise quand même les promises pour faciliter les tests). Cette fonction est censée envoyer les données username et password dans une requête POST HTTPS vers le service localhost/auth et comme c’est une fonction asynchrone de type Promise, elle peut être enchaînée avec un .then, un .catch, un .finally, être synchronisée avec un await, ou même être utilisée dans un Promise.all(). Rassurez-vous, il n’y aura pas besoin de créer un service auth sur votre localhost pour tester ce script.

Pour information, les fonctions ici ont la syntaxe des fonctions fléchées (arrow functions) introduites par ES6, et proches des expressions lambda que l’on trouve à peu près partout à présent :

const authenticate = (username, password) => new Promise((resolve, reject) => {…})

est équivalent à :

function authenticate(username, password) {     return new Promise(function promise(resolve, reject){…});
}

On ajoute donc ce fichier authentication.js dans notre projet, de préférence dans un dossier src, et l’on crée également un fichier authentication.spec.js, pour l’instant vide. Votre projet devrait ressembler à :

| node_modules/
| src/
|   | authenticate.js
|   | authenticate.spec.js
| package-lock.json
| package.json

3.2 Authentication.spec.js

Pour le fichier authentication.spec.js, on va commencer par un test simple, le null or undefined test : tester ce qui se passe lorsque certains champs sont nuls, ce qui généralement promet quelques erreurs null reference ou undefined object.

Un fichier de test contient une ou plusieurs séries de tests (describe), et dans ces blocs describe un ou plusieurs tests (test, ou it, un alias de test à utiliser si vous voulez nommer vos tests en incluant it, comme dans it must do something). Ensuite dans ces tests, on vérifie le comportement avec des assertions expect, qui utilisent une syntaxe proche de Chai, proche aussi du BDD (Behavior-Driven Development).

Si vous avez un éditeur de code particulier, c’est le moment de vérifier s’il n’y a pas quelques extensions pour Jest, comme Jest de Orta sous Visual Code [6].

On va donc tester le comportement de la fonction lorsque ses paramètres sont nuls. Notre premier fichier de test devrait ressembler à (authentication.spec.js) :

const { authenticate } = require('./authenticate')
 
describe('authenticate', () => {
      it('must reject on null', async () => {
        await expect(authenticate(null, null)).rejects.toBeTruthy()
    })
})

Ici, on utilise expect avec .rejects.toBeTruthy() pour vérifier que la promise est rejetée (rejects) avec n’importe quelle valeur (toBeTruthy()), et un await dans un test async pour attendre la fin de la promise (cf. l’API expect [7] et l’exemple asynchrone officiel [8]).

En principe, vous ne devriez pas avoir de service auth qui tourne en local, et le résultat suivant devrait s’afficher en lançant la commande npm run test :

$ npm run test
 
> test@1.0.0 test
> jest
 
PASS src/authenticate.spec.js
  authenticate
    √ must reject on null (35 ms)
 
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.759 s, estimated 1 s
Ran all test suites.

C’est un bon début, mais ce n’est pas suffisant ; qui nous dit que la fonction n’a pas été rejetée plutôt à cause de la connexion HTTP ? Pour savoir cela, on va vérifier le code retour de l’erreur. L’une des façons de vérifier les erreurs est de tester le bloc catch de la promise :

const { authenticate } = require('./authenticate')
 
describe('authenticate', () => {
    it('must reject on null', async () => {
        await authenticate(null, null).catch(error => {
            console.log(error);
            expect(error.code).not.toBe('ECONNREFUSED')
        });
    })
})

Cette fois, le test ne passe plus, en effet si la fonction rejette bien une erreur, que l’on récupère dans un catch plutôt qu’avec un expect rejects, le code d’erreur ne devrait pas être (.not.toBe) ECONNREFUSED.

FAIL src/authenticate.spec.js
  authenticate tests
    × must reject on null (62 ms)
 
  ● authenticate › must reject on null
 
    expect(received).not.toBe(expected) // Object.is equality
 
    Expected: not "ECONNREFUSED"
 
       6 |         const res = await authenticate(null, null).catch(error => {
       7 |             console.log(error);
    > 8 |             expect(error.code).not.toBe('ECONNREFUSED')
         |                                    ^
       9 |         });
      10 |
      11 |         expect(res).toBeUndefined()
 
      at src/authenticate.spec.js:8:36
      at Object.<anonymous> (src/authenticate.spec.js:6:21)
 
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total

Pour continuer à tester cette fonction, on a le choix soit de mettre en place un service auth en local juste pour les tests, ce qui serait overkill comme dirait un collègue, soit de feinter en réalisant de fausses requêtes HTTP grâce aux mocks. Et c’est très exactement cette seconde solution que l’on va mettre en place, puisqu’en plus d’être puissants et extrêmement utilisés dans les tests unitaires, tous langages confondus, les mocks sont vraiment faciles à utiliser avec Jest. Par ailleurs, les tests unitaires doivent en principe être isolés du reste du système, et donc d’être dépendants de bases de données et autres services.

4. Les mocks

4.1 Présentation

Du nom de la tortue fantaisie (mock turtle) d’Alice au pays des merveilles, les mocks sont des objets de fantaisie, ou factices, qui remplacent les objets concrets. Ils sont très utiles pour les tests unitaires lorsque l’on souhaite juste simuler une partie d’un objet plutôt que d’instancier toutes ses dépendances. Ils réduisent aussi le scope des tests unitaires à l’unité testée en question. Par exemple, durant les tests unitaires, en principe on mocke les accès aux bases de données. De fait, on peut également définir les tests unitaires avant le développement en mockant les dépendances externes et en définissant les spécifications des fonctions à développer, dans une approche TDD (Test Driven Development) ou XP (Extreme Programming), un développement couplé avec des tests unitaires. Comme nous le verrons par la suite, les mocks sont indispensables pour la plupart des tests unitaires [9, 10].

Il est préférable de différencier les mocks des stubs et des spys, ce sont trois choses différentes, bien que leurs définitions puissent sensiblement varier d’un auteur à un autre, voire d’un framework de test à un autre.Les stubs sont des simulacres de fonctions qui vont venir temporairement remplacer les vraies fonctions ; ce sont des stubs de fonctions, pas d’objets, contrairement aux mocks. C’est l’équivalent d’un mock partiel.Les spys sont des wrappeurs qui font vérifier l’appel d’un objet, d’une fonction, sans la transformer, en principe. Très utile pour être certain qu’une fonction lointaine a bien été appelée, et combien de fois. On peut également faire le spy d’un mock ou d’un stub. Certains frameworks font un stub de la fonction spy ; sous Jest, c’est un comportement optionnel.

4.2 Utilisation

Avec Jest, les mocks sont inclus dans le framework ; il suffit de faire appel à jest.mock après l’import (ou le require) du module à mocker. Par défaut, jest.mock va mocker l’ensemble du module, mais on peut également faire un mock partiel de quelques fonctions. Les fonctions mockées renvoient des valeurs par défaut, mais on peut aussi les spécifier, que ce soit pour un objet, une fonction asynchrone (promise), un observateur.

Dans l’exemple précédent, nous avions besoin de mocker l’appel vers un webservice d’authentification pour compléter les tests unitaires. Pour mocker le module HTTPS, on va appeler jest.mock(‘https’) après nos imports, et le mock sera automatiquement utilisé dans le script authenticate à la place du véritable module HTTPS.

const { authenticate } = require('./authenticate')
jest.mock('https')
 
describe('authenticate', () => {
   await authenticate(null, null).catch(error => {
      console.log(error);
      expect(error).toBeUndefined();
  });}

Cependant, le résultat de la requête n’est pas défini, et la requête va nous renvoyer une erreur de type res undefined. Nous allons donc affiner le mock de la méthode request en définissant une valeur de retour.

const { authenticate } = require('./authenticate')
 
jest.mock('https', () => ({
    request:(options, res) => res({
        on: (data, res) => res(Buffer.from('{"success":true}'))
    })
}))
 
describe('authenticate', () => {
    it('must fail on null', async () => {
        const res = await authenticate(null, null).catch(error => {
            console.log(error);
            expect(error).toBeUndefined()
        });
        expect(res.success).toBe(false)
    })
})

Cette fois, le test passe, le mock HTTPS renvoie bien nos data (ici, le JSON {"success":true}).

Il ne reste plus qu’à affiner le test pour un utilisateur valide ou non. Pour ce faire, on va aller au plus simple en passant un booléen au mock pour lui dire si cet utilisateur doit être valide au niveau HTTPS. Pour passer une variable externe au mock, il faut le préfixer par mock (ex : mockIsValidUser) ; c’est une sécurité Jest. On obtient le fichier de test authentication.spec.js suivant :

const { authenticate } = require('./authenticate')
 
mockIsValidUser = false;
jest.mock('https', () => ({
    request:(options, res) => res({
        on: (data, res) => {
            if(mockIsValidUser){
                res(Buffer.from('{"success":true}'))
            }else{
                res(Buffer.from('{"success":false}'))
            }
        }
    })
}))
 
describe('authenticate', () => {
    it('must fail on null', async () => {
        mockIsValidUser = false
        const res = await authenticate(null, null)
        expect(res.success).toBe(false)
    })
 
    it('must fail on bad user', async () => {
        mockIsValidUser = false
        const res = await authenticate("tuut", "1234")
        expect(res.success).toBe(false)
    })
 
    it('must be success on valid user', async () => {
        mockIsValidUser = true
        const res = await authenticate("tester", "pass")
        expect(res.success).toBe(true)
    })
})
$ npm run test
 
> test@1.0.0 test
> jest
 
PASS src/authenticate.spec.js
  authenticate
    √ must fail on null (10 ms)
    √ must fail on bad user (1 ms)
    √ must be success on valid user (1 ms)
 
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.839 s, estimated 3 s
Ran all test suites.

On a ainsi vérifié que la fonction authenticate renvoie success à true uniquement pour un utilisateur valide. Vous me direz que c’est un peu facile puisque l’on indique si l’utilisateur est valide ; sauf que le point important ici, c’est que l’on n’a pas modifié la fonction authenticate, mais juste les résultats de la requête HTTPS utilisée à l’intérieur de cette fonction pour simuler, pour mocker, la sortie. Bien évidemment, on peut ajouter davantage de tests pour s’assurer que la fonction répond bien aux spécifications, aux exigences fonctionnelles.

5. TypeScript

Avec les versions récentes de Jest, TypeScript est pleinement supporté, moyennant un peu de configuration. Un projet TypeScript sous Node.js ressemble à un projet JavaScript, mais avec le module typescript installé et un fichier de configuration tsconfig.json. TypeScript se chargera de transformer, de transcompiler les fichiers ts en fichiers js, automatiquement ou manuellement avec la commande tsc. L’avantage principal de TypeScript par rapport à JavaScript, c’est l’utilisation des types et des classes, et donc de modèles d’objets typés, ce qui nous permet d’avoir moins d’erreurs d’objets ou d’attributs non définis, de vérifier le type des données, et d’avoir ainsi un code plus robuste. Pour passer de JavaScript à TypeScript, installez le module typescript et créez un fichier tsconfig.json à la racine du projet. Pour utiliser Jest avec TypeScript, il faudra aussi installer les modules ts-jest et @types/jest, puis créer un fichier de config jest.config.js à la racine du projet.

$ npm i --save-dev typescript ts-jest @types/jest

Fichier tsconfig.json :

{{
  "compilerOptions": {
      "baseUrl": ".",
      "outDir": "./built",
      "allowJs": true,
      "target": "es5",
      "sourceMap": true,
      "resolveJsonModule": true,
      "allowSyntheticDefaultImports": true,
      "esModuleInterop": true,
    }
}

Fichier jest.config.js :

module.exports = {
    preset: "ts-jest",
    transform: {
      "^.+\\.(ts|tsx)?$": "ts-jest",
    },
};

Nommez les fichiers TypeScript en principe avec l’extension .ts et leurs fichiers de tests avec l’extension .spec.ts. Pour plus d’informations à propos des options du fichier tsconfig.json, voir la page de référence [11].

Pour l’exemple, nous allons reprendre la fonction authenticate, mais cette fois en TypeScript, et en utilisant une bibliothèque de requêtes HTTP comme Axios [12].

$ npm i axios

Fichier authenticate.ts :

import axios from 'axios';
 
const url = 'http://localhost:443/auth'
 
const authenticate = (username:string, password:string) => axios.post(url, {
        username: username,
        password: password
    })
    
export { authenticate }

Avec une bibliothèque, le code devient tout de suite beaucoup plus lisible, concis et plus maintenable.

Pour le fichier de tests unitaires authenticate.spec.ts, comme en JavaScript il faudra mocker l’appel externe, toujours avec jest.mock, sauf qu’ici on va mocker la bibliothèque Axios [13].

Fichier authenticate.spec.ts :

import { authenticate } from "./authenticate";
 
var mockIsValidUser = false;
jest.mock('axios', () => ({
post: () => ({ data : {success: mockIsValidUser}})
}))
 
describe('authenticate', () => {
    it('must fail on null', async () => {
        mockIsValidUser = false;
        const res = await authenticate(null,null);
        expect(res.data.success).toBe(false)
    })
 
    it('must be success on valid user', async () => {
        mockIsValidUser = true
        const res = await authenticate("tester", "pass")
        expect(res.data.success).toBe(true)
    })
})
$ npm run test
 
> test@1.0.0 test
> jest
 
PASS src/authenticate.spec.ts (5.134 s)
  authenticate
    √ must fail on null (6 ms)
    √ must be success on valid user (1 ms)
 
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        5.634 s, estimated 6 s
Ran all test suites.

Encore une fois, même si on a mocké la fonction post d’Axios, nous n’avons pas touché à la fonction authenticate, ce qui nous permet de tester cette fonction avec diverses valeurs retournées et simulées de post. Dans ce test, il faut ajouter une propriété data comme indiqué par le type de retour de l’API Axios.

6. Couverture de code

Pour la couverture de code, c’est-à-dire la vérification du taux de code couvert par les tests unitaires – un indicateur de qualité de logiciel de plus en plus présent dans les projets – il suffit d’ajouter l’option --coverage à l’appel de Jest, par exemple dans votre fichier package.json :

...
"scripts": {
    "test": "jest",
    "test:coverage" : "jest --coverage"
  },...

Ensuite, vous pouvez lancer la couverture de code avec :

$ npm run test:coverage
 
> test@1.0.0 test:coverage
> jest --coverage
 
PASS src/authenticate.spec.ts
  authenticate
    √ must fail on null (3 ms)
    √ must be success on valid user (1 ms)
 
-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |
authenticate.ts  |     100 |      100 |     100 |     100 |
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.586 s, estimated 5 s
Ran all test suites.

Il existe aussi d’autres rendus de couverture de code, comme le rendu HTML, en ajoutant l’option coverageReporters dans votre fichier jest.config.js. Jest supporte tout reporter Istanbul. Vous pouvez même configurer un seuil, une limite minimale de couverture à atteindre pour valider les tests, avec l’option coverageThreshold. L’option collectCoverage lancera la couverture de code par défaut [14].

Exemple de fichier jest.config.js, avec une couverture minimale de 80 %:

module.exports = {
    "collectCoverage": true,
    "coverageReporters": ["text", "lcov", "html"],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,        "statements": 80
      }
    }
  };

Le reporter html produit un rapport HTML interactif dans le dossier coverage (à ajouter dans le .gitignore).

jest figure 01.png-s

7. Astuces

7.1 Activer, désactiver des tests unitaires

Pour désactiver un test unitaire, pour ne pas avoir à le rejouer sans cesse, vous pouvez utiliser un skip après un describe ou après un it (ou un test) ; mais vous pouvez aussi utiliser only sur un test ou un describe, ce qui désactivera les autres tests et suites de tests. Par exemple :

describe.only('authenticate', () => {
    it.skip('must fail on null', async () => {
      ...
    })
 
    it.only('must be success on valid user', async () => {
      ...
    })
})

7.2 Tester la structure de données

Pour tester la structure de données, il existe plusieurs possibilités. Une première possibilité est de tester les champs un par un, ce qui peut être fastidieux sur les grosses structures ; une seconde est de vérifier l’égalité stricte entre une structure modèle et la structure à tester avec un expect(data).toStrictEqual(model), ce qui est très efficace avec les tableaux et les objets ; on peut également tester le type des données, si elles sont typées (sous TypeScript) avec .toBeInstanceOf et dans le cas contraire, il est aussi possible de vérifier le schéma des données JSON.

Pour tester le schéma des données JSON, vous aurez besoin du package jest-json-schema d’American Express (et de @types/jest-json-schema sous TypeScript). Une fois installé, il faudra étendre les expect avec expect.extend(matchers) et vous pourrez utiliser la méthode .toMatchSchema(value). Par exemple, sous TypeScript :

$ npm i --save-dev jest-json-schema @types/jest-json-schema
import { matchers } from 'jest-json-schema';
import { authenticate } from "./authenticate";
 
expect.extend(matchers)
 
describe('authenticate', () => {
    it('must validate schema', async () => {
        const schema = {
            type: 'object',
            properties: {
                data: {
                  type: "object",
                  properties: {
                    success: { type: "boolean" }
                  },
                  required: [ "success" ]
                }
              },
              required: [ "data" ]
        }
        const res = await authenticate("tester", "pass")
        expect(res).toMatchSchema(schema)
    })
})

Sur le site de liquid-technologies, vous trouverez un utilitaire en ligne pour générer un schéma à partir de données JSON ; à vous ensuite de le customiser selon vos contraintes. Vous trouverez aussi des informations à propos des schémas sur https://json-schema.org [15, 16, 17].

7.3 Tester les promises et les observables

Pour mocker les promises, on l’a vu dans les exemples précédents, on peut mocker, ou stuber, la fonction dans le mock du module, exemple post : () => ({ data : {success: mockIsValidUser}}), mais on peut aussi utiliser la méthode .mockResolvedValue(value) ou .mockRejectedValue(value). Pour les tester, vous pouvez utiliser un expect .resolves ou .rejects. Vous trouverez quelques exemples sur le site de Jest [7, 13].

Pour mocker les observables, vous pouvez utiliser la fonction of de la bibliothèque RxJS ; et pour les tester, on peut les convertir en promises avec les fonctions firstValueFrom ou lastValueFrom de la bibliothèque RxJS, puis utiliser les méthodes précédentes.

7.4 Tester les exceptions

Pour les exceptions, la fonction toThrowError permet de les tester. Placez la fonction à tester dans une fonction fléchée, pour qu’expect intercepte l’exception, sauf si c’est une promise, auquel cas il faut ajouter un .rejects avant.

Exemples :

// fonction synchrone :
expect(() => mafonc()).toThrowError(Error(‘undefined parameter’))
// Promise :
expect(maprom()).rejects.toThrowError(Error(‘undefined parameter’))

7.5 Tester les composants Angular

Les tests unitaires sous Angular sont un sujet à part entière, aussi nous n’entrerons pas dans les détails dans cet article. Pour résumer, il vous faudra désinstaller Karma, le moteur de test unitaire natif d’Angular, puis installer et configurer Jest pour Angular. Ensuite le mock des services est simplifié sous Jest, puisqu’il suffit de créer un objet quelconque et ses fonctions ou méthodes avec Jest.fn() et vous avez votre service mocké. Vous pouvez ensuite l’insérer dans les providers de TestBed.

Exemple :

const mockUserService = {
    authenticate : jest.fn(() => ({ data : {success: true}}))
    otherFunc : jest.fn()
}
beforeEach(() => {
    TestBed.configureTestingModule({
   …
    providers: [ { provide: UserService, useValue: mockUserService} ]
   …
   }).compileComponents();
})

Ensuite pour les composants, vous pouvez utiliser les expects comme précédemment. Par contre si vous avez des appels asynchrones (listes déroulantes, etc.), la méthode whenStable() de la fixture vous sera utile.Exemple :

    beforeEach(() => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    })
 
    it('should be initialized', async ()=>{
        fixture.whenStable().then(() => {
            expect(component.userId).toBeGreaterThan(0)
        })
    })

7.6 Tester les requêtes Node.js

Pour les webservices sous Node.js, vous aurez sûrement des fonctions de requêtes HTTP du type func(req, res, next) à tester. Pour les tester unitairement, on peut mocker la requête HTTP et vérifier le retour de la fonction avec le package node-mocks-http. Vous trouverez plus d’informations et des exemples sur la page de node-mocks-http. Vous pouvez aussi tester les routes sous Jest avec SuperTest [18, 19].

Conclusion

Nous avons vu les principaux points des tests unitaires avec Jest, un moteur de test unitaire pour JavaScript et TypeScript devenu incontournable pour ces langages. Nous n’avons pas listé toutes les possibilités offertes par ce framework et ses nombreuses extensions, mais vous trouverez davantage d’informations sur les sites officiels, les liens référencés et le Web en général. Il ne reste plus qu’à tester.

Références

[1] Wikipédia, Test, https://fr.wikipedia.org/wiki/Test_(informatique)

[2] Stack Overflow, Most Popular Technologies (2021),
https://insights.stackoverflow.com/survey/2021#technology-most-popular-technologies

[3] NPM Docs, npm-inithttps://docs.npmjs.com/cli/v8/commands/npm-init

[4] NPM Docs, package.json, https://docs.npmjs.com/cli/v8/configuring-npm/package-json

[5] Jest, testMatch, https://jestjs.io/docs/configuration#testmatch-arraystring

[6] Marketplace, vscode-jest, https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest

[7] Jest, expect, https://jestjs.io/docs/expect

[8] Jest, an async example, https://jestjs.io/docs/tutorial-async

[9] Wikipédia, Mockhttps://fr.wikipedia.org/wiki/Mock_(programmation_orientée_objet)

[10] Wikipédia, Mock object, https://en.wikipedia.org/wiki/Mock_object

[11] TypeScript, tsconfig reference, https://www.typescriptlang.org/tsconfig

[12] NPM, Axios, https://www.npmjs.com/package/axios

[13] Jest, Mock Functions, https://jestjs.io/docs/mock-functions

[14] Jest, Coverage Reporters,
https://jestjs.io/docs/configuration#coveragereporters-arraystring--string-options

[15] American Express, jest-json-schema, https://github.com/americanexpress/jest-json-schema

[16] Liquid Technologies, online json-to-schema converter,
https://www.liquid-technologies.com/online-json-to-schema-converter

[17] JSON Schema, JSON Schema, https://json-schema.org/

[18] Howard Abrams, node-mocks-http, https://github.com/howardabrams/node-mocks-http

[19] VisionMedia, SuperTest, https://github.com/visionmedia/supertest



Article rédigé par

Par le(s) même(s) auteur(s)

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 65 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous