Chapitre 1315 minIntermédiaire
Tests de performance, de charge et de sécurité
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- distinguer test de charge, de stress, de pic et d'endurance ;
- écrire un scénario k6 avec des seuils et lire un p95 ;
- automatiser les tests de contrôle d'accès d'une API ;
- respecter les règles d'un test de charge autorisé.
4 exercices corrigés
Les tests non fonctionnels vérifient comment l'application rend service, et pas seulement ce qu'elle fait : est-elle assez rapide quand beaucoup d'utilisateurs se connectent en même temps (tests de performance et de charge) ? Résiste-t-elle aux usages malveillants (tests de sécurité) ? Ce chapitre montre comment mesurer les performances de l'API de Prêt'Matos avec k6, lire ses indicateurs, et quels tests de sécurité un développeur automatise lui-même.
Les familles de tests de performance
| Test | Question posée | Scénario pour Prêt'Matos |
|---|---|---|
| Test de charge (load test) | L'application tient-elle la charge prévue ? | 20 salariés consultent le catalogue et réservent en même temps, pendant 5 minutes |
| Test de stress (stress test) | Où est sa limite, et comment se comporte-t-elle au-delà ? | On monte progressivement jusqu'à 300 utilisateurs simultanés pour voir à partir de quand les erreurs apparaissent |
| Test de pic (spike test) | Supporte-t-elle une hausse brutale ? | Le lundi à 9 h, tout le monde réserve en même temps pour la semaine |
| Test d'endurance (soak test) | Tient-elle dans la durée ? | 20 utilisateurs pendant 8 heures : une fuite de mémoire ou de connexions finit par se voir |
Pour une application interne de 14 salariés, le test de charge suffit souvent : il vérifie que l'application répond correctement à la charge réaliste, avec une marge. Les autres servent pour un service exposé au public, ou avant un événement prévisible (une campagne d'inscriptions, des soldes).
Les indicateurs à mesurer
| Indicateur | Ce qu'il mesure | Ce qu'on vise pour Prêt'Matos |
|---|---|---|
| Temps de réponse | Le délai entre l'envoi de la requête et la réception de la réponse | 95 % des requêtes en moins de 300 ms |
| Débit | Le nombre de requêtes traitées par seconde | De quoi absorber 20 utilisateurs actifs, avec de la marge |
| Taux d'erreur | La part des requêtes en échec (codes 5xx, délais dépassés) | Moins de 1 % |
| Ressources du serveur | Processeur, mémoire, connexions à la base | Pas de saturation, pas de croissance continue de la mémoire |
Un test de charge avec k6
k6, édité par Grafana Labs, est un outil libre de test de charge. Le scénario s'écrit en JavaScript ; l'outil, lui, est un programme autonome écrit en Go, qui s'installe à part (pas avec npm) : brew install k6 sous macOS, winget install k6 --source winget sous Windows, le paquet de la distribution sous Linux, ou l'image Docker grafana/k6. La version 2 est sortie en 2026.
// charge/catalogue.js : 20 utilisateurs qui consultent le catalogue
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL ?? 'http://localhost:3100';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // montée progressive à 20 utilisateurs
{ duration: '1m', target: 20 }, // 20 utilisateurs pendant une minute
{ duration: '10s', target: 0 }, // descente
],
thresholds: {
http_req_failed: ['rate<0.01'], // moins de 1 % d'erreurs
http_req_duration: ['p(95)<300'], // 95 % des requêtes en moins de 300 ms
},
};
export default function () {
const reponse = http.get(`${BASE_URL}/api/materiels`, {
headers: { Authorization: 'Bearer jeton-julie' },
});
check(reponse, { 'statut 200': (r) => r.status === 200 });
sleep(1); // un utilisateur réel réfléchit entre deux clics
}
| Élément | Rôle |
|---|---|
export default function | Le parcours d'un utilisateur virtuel (VU) ; k6 le répète en boucle pour chaque VU |
stages | Le profil de charge : le nombre d'utilisateurs virtuels monte, reste stable, puis redescend |
check | Une vérification sur chaque réponse ; le résultat s'affiche en pourcentage de réussite, sans arrêter le test |
thresholds | Les critères de réussite : si l'un n'est pas atteint, k6 termine avec un code de retour d'erreur, ce qui fait échouer une étape de CI |
sleep(1) | Une pause entre deux requêtes, pour simuler un humain ; sans elle, chaque VU enverrait des requêtes en rafale |
__ENV.BASE_URL | L'adresse de l'environnement testé, passée au lancement : k6 run -e BASE_URL=https://recette.pretmatos.example charge/catalogue.js |
On lance le test avec k6 run charge/catalogue.js. À la fin, k6 affiche un résumé : chaque seuil avec une coche ou une croix, puis les métriques. Les plus importantes :
| Métrique de k6 | Sens |
|---|---|
http_req_duration | Le temps de réponse, avec la moyenne, le minimum, la médiane, le maximum, p(90) et p(95) |
http_req_failed | La part des requêtes en échec |
http_reqs | Le nombre total de requêtes, et le débit par seconde |
checks | Le taux de réussite des vérifications |
vus, iterations | Le nombre d'utilisateurs virtuels, et de parcours complets effectués |
Pour que les mesures veuillent dire quelque chose, l'environnement de test doit ressembler à la production : même type de serveur, même base, un volume de données réaliste (une table de 3 réservations ne révèle pas l'index manquant qui ralentira la table de 30 000 lignes). Et le scénario doit ressembler à l'usage : quelques réservations pour beaucoup de consultations du catalogue, pas l'inverse.
Interpréter un résultat et trouver le goulet
Un test de charge ne dit pas pourquoi l'application est lente, seulement quand. Si le seuil p95 n'est pas atteint, on cherche le goulet d'étranglement :
| Symptôme | Cause fréquente | Piste |
|---|---|---|
| Le temps de réponse augmente avec le nombre d'utilisateurs, la base est très sollicitée | Une requête SQL sans index, ou des requêtes en boucle (problème N+1) | EXPLAIN sur la requête, ajout d'un index (index SQL) |
| Les erreurs apparaissent d'un coup à partir de N utilisateurs | Le pool de connexions à la base est trop petit, ou le serveur manque de mémoire | Taille du pool, ressources du serveur, journaux |
| Le temps de réponse augmente lentement pendant un test d'endurance | Une fuite de mémoire, des connexions jamais fermées | Supervision de la mémoire du processus (supervision) |
| Une seule route est lente | Un calcul coûteux ou un appel externe lent dans cette route | Mesurer la route seule, mettre en cache, rendre l'appel asynchrone |
Les tests de sécurité
Les tests de sécurité vérifient que l'application résiste aux usages malveillants. Certains relèvent de spécialistes : la revue de code orientée sécurité, l'analyse statique (SAST), l'analyse de l'application en fonctionnement (DAST), l'analyse des dépendances (SCA) et le test d'intrusion, toujours mené avec une autorisation écrite et un périmètre défini. Le cours Sécurité applicative les présente (tests de sécurité). Mais une grande partie des tests de sécurité sont des tests automatisés ordinaires, que le développeur écrit lui-même avec les outils des chapitres précédents.
| Ce qu'on vérifie | Le test | Risque couvert |
|---|---|---|
| Une route protégée refuse l'accès sans authentification | Supertest sans jeton : 401 | Accès anonyme à des données |
| Un rôle ne peut pas faire ce qui est réservé à un autre | Un salarié sur la route de sortie : 403, et rien n'est modifié | Élévation de privilèges |
| Un utilisateur ne peut pas lire ou modifier les données d'un autre | Julie demande la réservation n° 1, qui appartient au salarié n° 5 : 403 ou 404 | IDOR (référence directe à un objet), la faille de contrôle d'accès la plus courante |
| Une entrée invalide est refusée proprement | Une date mal formée : 400, avec un message générique | Erreurs internes exposées, comportements inattendus |
| Une réponse ne contient pas de donnée sensible | expect(reponse.body).not.toHaveProperty('mdp_hash') | Exposition excessive de propriétés (OWASP API Security) |
| Un jeton expiré ou falsifié est refusé | La fonction de vérification de jeton reçoit un jeton expiré : null, puis 401 | Contournement de l'authentification |
| Les dépendances n'ont pas de faille connue | npm audit, composer audit, dotnet list package --vulnerable dans la CI | Bibliothèques vulnérables |
Ces tests ont un grand avantage sur un audit ponctuel : ils sont rejoués à chaque modification. Une route ajoutée sans son middleware d'authentification fait échouer la CI le jour même, au lieu d'attendre le prochain audit. C'est la non-régression appliquée à la sécurité.
Accessibilité et compatibilité
Deux autres familles de tests non fonctionnels concernent directement une application web :
- l'accessibilité : un outil automatique comme axe détecte une partie des défauts (chapitre 9) ; le reste se vérifie à la main, au clavier et avec un lecteur d'écran. En France, le référentiel RGAA décrit les critères à respecter, obligatoires pour les services publics et les grandes entreprises ; depuis le 28 juin 2025, la directive européenne sur l'accessibilité (transposée par la loi du 9 mars 2023) étend l'obligation à de nombreux services numériques privés, comme le commerce en ligne, la banque ou le transport ;
- la compatibilité : Playwright exécute les mêmes tests sur Chromium, Firefox et WebKit, et en taille téléphone, en ajoutant des projets dans sa configuration.
Exercices
Exercice 1 sur 4 Lire les résultats d'un test de charge Réussi À revoir
Voici les résultats de deux tests de charge de l'API, avant et après l'ajout de l'historique des prêts. Le critère est : p95 sous 300 ms et moins de 1 % d'erreurs.
| Mesure | Version 1.4 | Version 1.5 |
|---|---|---|
| Temps de réponse moyen | 85 ms | 140 ms |
| p95 | 190 ms | 1 250 ms |
| Taux d'erreur | 0,0 % | 0,4 % |
| Requêtes par seconde | 19,6 | 19,2 |
- La version 1.5 respecte-t-elle le critère ?
- Pourquoi la moyenne ne suffisait-elle pas à le voir ?
- Quelle hypothèse vérifierais-tu en premier ?
Voir la réponseMasquer la réponse de l'exercice 1
- Non : le taux d'erreur (0,4 %) est sous 1 %, mais le p95 (1 250 ms) dépasse largement 300 ms.
- La moyenne passe de 85 à 140 ms, ce qui semble acceptable. Mais 5 % des requêtes prennent plus de 1,25 s : quelques requêtes très lentes sont noyées dans une majorité de requêtes rapides. Le p95 révèle cette « queue » de requêtes lentes.
- La nouveauté de la version 1.5 est l'historique des prêts : on cherche une requête SQL lente, par exemple sans index sur la colonne de filtre, ou une boucle qui exécute une requête par prêt (problème N+1). On mesure la route de l'historique seule, puis on examine sa requête avec
EXPLAIN.
Tu avais trouvé ?
Exercice 2 sur 4 Quel test de performance ? Réussi À revoir
Pour chaque situation, choisis entre test de charge, de stress, de pic et d'endurance.
- La mairie ouvre les inscriptions à la cantine le 1er septembre à 8 h : 2 000 familles se connectent dans le premier quart d'heure.
- Le service tourne sans redémarrage du lundi au vendredi ; on soupçonne une fuite de mémoire.
- Avant la mise en service, on veut vérifier que l'application supporte les 14 salariés de Sorbier Logistique en même temps.
- L'hébergeur demande jusqu'à combien d'utilisateurs simultanés le serveur actuel suffit.
Voir la réponseMasquer la réponse de l'exercice 2
- Test de pic : une hausse brutale et prévisible.
- Test d'endurance : une charge normale, longtemps, en surveillant la mémoire.
- Test de charge : la charge prévue, avec une marge.
- Test de stress : on monte jusqu'à la limite pour la connaître.
Tu avais trouvé ?
Exercice 3 sur 4 Écrire un test de sécurité Réussi À revoir
L'équipe ajoute la route GET /api/reservations/:id, qui renvoie le détail d'une réservation. Règle : un salarié ne voit que ses propres réservations ; la logistique voit tout. Avec le jeu de données du chapitre 8 (la réservation n° 1 appartient au salarié n° 5), écris avec Supertest les trois tests de contrôle d'accès de cette route. Tu supposeras que l'API répond 404 quand un salarié demande la réservation d'un autre.
Voir la réponseMasquer la réponse de l'exercice 3
describe('GET /api/reservations/:id', () => {
test('renvoie 401 sans jeton', async () => {
const reponse = await request(app).get('/api/reservations/1');
expect(reponse.status).toBe(401);
});
test("renvoie 404 quand un salarié demande la réservation d'un autre", async () => {
const reponse = await request(app).get('/api/reservations/1').set('Authorization', 'Bearer jeton-julie');
expect(reponse.status).toBe(404);
expect(reponse.body).not.toHaveProperty('materiel');
});
test('renvoie la réservation à la logistique', async () => {
const reponse = await request(app).get('/api/reservations/1').set('Authorization', 'Bearer jeton-nadia');
expect(reponse.status).toBe(200);
expect(reponse.body).toMatchObject({ id: 1, materiel: 'PC-07', salarie: 5 });
});
});
Répondre 404 plutôt que 403 évite de confirmer à un curieux qu'une réservation n° 1 existe. Le deuxième test vérifie aussi qu'aucune donnée ne fuit dans le corps de la réponse. Il manquerait un quatrième test pour le cas nominal du salarié : il voit sa réservation (il faut alors en ajouter une à son nom dans le jeu de données, ou dans le test).
Tu avais trouvé ?
Exercice 4 sur 4 Fonctionnel ou non fonctionnel ? Réussi À revoir
Classe chaque exigence du cahier des charges de Prêt'Matos et propose le test qui la vérifierait.
- « La liste des matériels s'affiche en moins de 2 secondes au guichet. »
- « Un salarié ne peut annuler que ses propres réservations. »
- « L'application est utilisable au clavier seul. »
- « Un prêt dure au plus 15 jours. »
Voir la réponseMasquer la réponse de l'exercice 4
- Non fonctionnelle (performance) : un test de charge avec un seuil sur le temps de réponse de la route, plus une mesure du chargement de la page en recette.
- Fonctionnelle, et de sécurité : un test d'intégration (Supertest) où un salarié tente d'annuler la réservation d'un autre, avec vérification que rien n'a été annulé.
- Non fonctionnelle (accessibilité) : un test manuel au clavier en recette, complété par une analyse axe automatique dans les tests de bout en bout.
- Fonctionnelle : des tests unitaires de
verifierReservationaux valeurs limites 15 et 16 jours.
Tu avais trouvé ?