8. Tests
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 simples, 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 complexes.
Les tests peuvent aussi servir à prévenir les soucis de régression. Imaginons que votre code évolue et qu'une fonctionnalité est atteinte par une modification de votre code sans que vous ne vous en rendiez compte. Les tests 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)retourne5.
-
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.
Vitest
Vitest est un framework de test moderne créé par l'équipe Vite. Il est extrêmement rapide, possède une excellente intégration TypeScript et une API compatible avec Jest, ce qui facilite la migration.
Installation
Commençons par installer Vitest :
npm install -D vitest supertest @types/supertest
Configuration
Créez un fichier vitest.config.ts à la racine de votre projet :
import {defineConfig} from 'vitest/config'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.config.ts',
'**/*.test.ts',
'**/*.spec.ts',
],
},
},
})
Options importantes :
globals: true: Permet d'utiliserdescribe,it,expectsans les importerenvironment: 'node': Environnement Node.js (au lieu de browser)coverage: Configuration de la couverture de code
Scripts package.json
Modifiez votre package.json en ajoutant ces scripts :
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
npm run test: Lance les tests en mode watchnpm run test:ui: Lance l'interface webnpm run test:coverage: Génère le rapport de couverture
Premier test unitaire
Commençons avec des tests unitaires très simples. 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éez un fichier tests/math.test.ts et ajoutez-y les lignes suivantes :
import {describe, expect, it} from 'vitest'
import {add} from '@/src/math'
describe('sum function', () => {
it('adds 1 + 2 to equal 3', () => {
const res = add(1, 2)
expect(res).toBe(3)
})
})
Explication des fonctions :
describe: Groupe plusieurs tests liés ensembleit: Définit un test individuel (alias detest)expect: Valeur à testertoBe: Matcher pour vérifier l'égalité stricte
Lancez la commande npm run test et vous verrez les tests s'exécuter en mode watch (réexécution automatique lors de
changements).
Vous trouverez la liste complète des matchers ici.
Matchers courants
Vitest propose de nombreux matchers pour valider vos tests :
// Égalité
expect(value).toBe(5) // Égalité stricte (===)
expect(value).toEqual({a: 1}) // Égalité profonde pour objets
// Booléens
expect(value).toBeTruthy() // Valeur truthy
expect(value).toBeFalsy() // Valeur falsy
expect(value).toBeNull() // Null
expect(value).toBeUndefined() // Undefined
// Nombres
expect(value).toBeGreaterThan(3) // Plus grand que
expect(value).toBeLessThan(5) // Plus petit que
expect(value).toBeCloseTo(0.3) // Proche de (pour floats)
// Strings
expect(string).toMatch(/pattern/) // Regex
expect(string).toContain('substring') // Contient
// Arrays
expect(array).toContain(item) // Contient un élément
expect(array).toHaveLength(3) // Longueur
// Objets
expect(obj).toHaveProperty('key') // Possède une propriété
expect(obj).toMatchObject({a: 1}) // Correspond partiellement
// Fonctions
expect(fn).toThrow() // Lève une erreur
expect(fn).toHaveBeenCalled() // A été appelée (avec mock)
Hiérarchisation des tests
Vous pouvez organiser vos tests avec plusieurs describe imbriqués :
import {describe, expect, it} from 'vitest'
import {add} from '@/math'
describe('Math operations', () => {
describe('sum function', () => {
it('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3)
})
it('adds -1 + 0 to equal -1', () => {
expect(add(-1, 0)).toBe(-1)
})
})
})
Hooks de lifecycle
Vitest propose des hooks pour exécuter du code avant/après les tests :
import {
describe,
it,
beforeAll,
beforeEach,
afterEach,
afterAll,
} from 'vitest'
describe('Database tests', () => {
// Exécuté une fois avant tous les tests
beforeAll(async () => {
await database.connect()
})
// Exécuté avant chaque test
beforeEach(async () => {
await database.clear()
})
// Exécuté après chaque test
afterEach(async () => {
await database.cleanup()
})
// Exécuté une fois après tous les tests
afterAll(async () => {
await database.disconnect()
})
it('should create a user', async () => {
// Test...
})
})
Coverage
La couverture de code permet de savoir quel pourcentage de votre code est testé.
Installation du provider
Pour activer la couverture, installez le provider v8 (recommandé) :
npm install -D @vitest/coverage-v8
Générer le rapport
Lancez la commande :
npm run test:coverage
Cela génère un rapport dans le terminal et un rapport HTML dans coverage/index.html.
Seuil de couverture
Vous pouvez définir un seuil minimum de couverture dans vitest.config.ts :
import {defineConfig} from 'vitest/config'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
exclude: [
'node_modules/',
'dist/',
'**/*.config.ts',
'**/*.test.ts',
'**/*.spec.ts',
],
},
},
})
Si le seuil n'est pas atteint, les tests échoueront en CI/CD.
Remarque: On appréciera généralement un seuil de couverture de 90% ou plus pour un projet en production.
Interface utilisateur
Vitest propose une interface web moderne pour visualiser vos tests :
npm run test:ui
Cela ouvre une interface dans votre navigateur avec :
- Liste des tests avec leur statut
- Détails des échecs
- Couverture de code visuelle
- Graphique de performance
Tester une API Express
Maintenant testons une vraie API Express avec Prisma.
Structure de l'application
Fichier src/index.ts (fichier principal) :
import express from 'express'
import {userRouter} from './user.route'
import {authRouter} from './auth.route'
export const app = express()
const port = 3000
// Middleware pour parser le JSON
app.use(express.json())
// Utilisation du router utilisateur
app.use('/users', userRouter)
app.use('/auth', authRouter)
// Démarrage du serveur (uniquement si le fichier est exécuté directement)
if (require.main === module) {
app.listen(port, () => {
console.log(`Mon serveur démarre sur le port ${port}`)
})
}
Important : On exporte
apppour pouvoir la tester sans démarrer le serveur. La conditionif (require.main === module)permet de démarrer le serveur uniquement lorsque le fichier est exécuté directement (pas lors des tests).
Mocking de Prisma
Le mocking est un concept qui permet de remplacer des modules ou des fonctions réels par des versions factices pendant les tests.
Pour tester sans toucher à la vraie base de données, nous allons mocker Prisma.
Installation
npm install -D vitest-mock-extended
Fichier de setup
Créez tests/vitest.setup.ts :
import {DeepMockProxy, mockDeep, mockReset} from 'vitest-mock-extended'
import {beforeEach, vi} from 'vitest'
// Import du client mocké
import prisma from '../src/client'
import {PrismaClient} from "@/generated/prisma/client";
// Mock du module Prisma
vi.mock('../src/client', () => ({
default: mockDeep<PrismaClient>(),
}))
// Mock du middleware d'authentification
vi.mock('../src/auth/auth.middleware', () => ({
authenticateToken: vi.fn((req, res, next) => {
// Simule un utilisateur authentifié
req.userId = 1
next()
}),
}))
// Reset des mocks avant chaque test
beforeEach(() => {
mockReset(prismaMock)
})
// Export du mock typé
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
Configuration Vitest
Modifiez vitest.config.ts pour charger le setup :
import {defineConfig} from 'vitest/config'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
globals: true,
environment: 'node',
setupFiles: ['./tests/vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.config.ts',
'**/*.test.ts',
'**/*.spec.ts',
],
},
},
})
Tests d'API avec Supertest
Créez tests/users.test.ts :
import {describe, expect, it} from 'vitest'
import request from 'supertest'
import {prismaMock} from "./vitest.setup";
import {app} from "@/index";
describe('GET /users', () => {
it('should return an array of users', async () => {
// Mock de la réponse Prisma
prismaMock.user.findMany.mockResolvedValue([
{
id: 1,
name: 'Alice',
email: 'alice@example.com',
password: 'hashedpassword',
},
{
id: 2,
name: 'Bob',
email: 'bob@example.com',
password: 'hashedpassword',
},
])
// Requête HTTP via supertest
const response = await request(app).get('/users')
// Assertions
expect(response.status).toBe(200)
expect(response.body).toHaveLength(2)
expect(response.body[0]).toHaveProperty('name', 'Alice')
})
it('should return empty array when no users', async () => {
prismaMock.user.findMany.mockResolvedValue([])
const response = await request(app).get('/users')
expect(response.status).toBe(200)
expect(response.body).toEqual([])
})
})
describe('POST /users', () => {
it('should create a new user', async () => {
const newUser = {
id: 1,
name: 'Charlie',
email: 'charlie@example.com',
password: 'hashedpassword',
}
prismaMock.user.create.mockResolvedValue(newUser)
const response = await request(app)
.post('/users')
.send({name: 'Charlie', email: 'charlie@example.com', password: 'password123'})
expect(response.status).toBe(201)
expect(response.body).toHaveProperty('message', 'Utilisateur créé')
expect(response.body.user).toHaveProperty('name', 'Charlie')
})
it('should return 400 for invalid data', async () => {
// Mock d'une erreur Prisma (par exemple, email déjà existant)
prismaMock.user.create.mockRejectedValue(new Error('Unique constraint failed'))
const response = await request(app)
.post('/users')
.send({name: 'Charlie', email: 'existing@example.com', password: 'password123'})
expect(response.status).toBe(400)
expect(response.body).toHaveProperty('error')
})
})
describe('GET /users/:id', () => {
it('should return a user by id', async () => {
prismaMock.user.findUnique.mockResolvedValue({
id: 1,
name: 'Alice',
email: 'alice@example.com',
password: 'hashedpassword',
})
const response = await request(app).get('/users/1')
expect(response.status).toBe(200)
expect(response.body).toHaveProperty('name', 'Alice')
expect(response.body).toHaveProperty('email', 'alice@example.com')
})
it('should return 404 when user not found', async () => {
prismaMock.user.findUnique.mockResolvedValue(null)
const response = await request(app).get('/users/999')
expect(response.status).toBe(404)
expect(response.body).toHaveProperty('error', 'Utilisateur non trouvé')
})
})
Mocking de fonctions
Vitest permet de mocker des fonctions facilement avec vi.fn() :
import {describe, it, expect, vi} from 'vitest'
describe('Callbacks', () => {
it('should call the callback function', () => {
const mockCallback = vi.fn()
function doSomething(callback: Function) {
callback('result')
}
doSomething(mockCallback)
// Vérifier que la fonction a été appelée
expect(mockCallback).toHaveBeenCalled()
expect(mockCallback).toHaveBeenCalledTimes(1)
expect(mockCallback).toHaveBeenCalledWith('result')
})
})