Les tests fonctionnels vérifient que le système fonctionne correctement. Les tests de performance vérifient qu'il fonctionne encore correctement sous pression. k6 comble cet écart.
Ce qu'est k6 et pourquoi les ingénieurs QA devraient le connaître
k6 est un outil de test de performance open source développé par Grafana Labs. Vous écrivez des scripts en JavaScript ou TypeScript, les lancez depuis la ligne de commande, et obtenez des données détaillées sur le comportement de votre système sous charge. Le dépôt GitHub dépasse les 24 000 étoiles et l'outil est utilisé dans des organisations de toutes tailles.
Ce qui fait de k6 le bon point d'entrée pour les ingénieurs QA, c'est son modèle de scripting. Si vous écrivez déjà des tests Playwright ou Jest, la syntaxe vous sera familière : vous importez des modules, écrivez des fonctions, faites des requêtes HTTP et ajoutez des assertions. Pas d'interface graphique lourde, pas de format d'enregistrement propriétaire, pas de fichier de configuration XML. Juste un script.
k6 s'intègre aussi proprement dans GitHub Actions. Vous installez le binaire, lancez votre script, et si le test échoue (on reviendra sur ce que "échouer" signifie), l'étape CI se termine avec un code non nul. C'est le même contrat que tout autre outil de test.
k6 ne remplace pas Playwright. Playwright vérifie le comportement ; k6 mesure les performances sous charge. Ils répondent à des questions différentes et ont tous les deux leur place dans une stratégie QA mature.
Installer k6 et lancer votre premier test
k6 est distribué sous forme de binaire autonome. Sur macOS :
brew install k6Sur Linux (Debian/Ubuntu) :
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6Sur Windows, utilisez l'installateur officiel depuis k6.io/docs/get-started/installation ou installez via Chocolatey :
choco install k6Une fois installé, vérifiez que ça fonctionne :
k6 versionCréez ensuite un fichier load-test.js avec ce script minimal :
import http from 'k6/http';
export default function () {
http.get('https://test-api.k6.io/public/crocodiles/');
}Lancez-le :
k6 run load-test.jsk6 utilise test-api.k6.io comme environnement de test public. Vous pouvez l'exécuter sans rien configurer. Vous verrez dans le terminal le nombre de requêtes, les temps de réponse et un tableau récapitulatif. C'est votre premier test de charge.
Anatomie d'un test k6
Un script k6 réel comporte trois parties. L'export options contrôle l'exécution du test, la fonction default contient la logique de test, et la configuration gère les requêtes. La structure :
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 10, // utilisateurs virtuels simultanés
duration: '30s', // durée d'exécution
};
export default function () {
http.get('https://your-api.example.com/api/products');
sleep(1); // temps de réflexion entre les requêtes
}vus désigne les utilisateurs virtuels. Chaque VU exécute la fonction default en boucle pendant toute la durée du test, indépendamment des autres. Avec vus: 10 et duration: '30s', 10 utilisateurs simulés frappent votre endpoint en continu pendant 30 secondes.
Le sleep(1) n'est pas facultatif : il modélise le temps de réflexion qu'un vrai utilisateur prendrait entre ses actions. Sans lui, chaque VU envoie des requêtes aussi vite que le serveur peut répondre, ce qui est irréaliste et produit des chiffres de débit trompeurs. Une seconde est une valeur raisonnable par défaut ; ajustez-la au comportement réel de vos utilisateurs.
Pour les requêtes POST avec un corps JSON, nécessaires pour la plupart des tests d'API :
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 20,
duration: '1m',
};
export default function () {
const url = 'https://your-api.example.com/api/login';
const payload = JSON.stringify({
username: 'testuser@example.com',
password: 'testpassword',
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
http.post(url, payload, params);
sleep(1);
}Lire la sortie k6
Après une exécution, k6 affiche un tableau récapitulatif dans le terminal. Savoir le lire rapidement transforme des chiffres bruts en informations exploitables.
Une sortie typique ressemble à ceci :
✓ checks.........................: 100.00% ✓ 1800 ✗ 0
data_received..................: 4.3 MB 143 kB/s
data_sent......................: 523 kB 17 kB/s
http_req_blocked...............: avg=1.2ms min=1µs med=4µs max=312ms p(90)=7µs p(95)=11µs
http_req_connecting............: avg=743µs min=0s med=0s max=181ms p(90)=0s p(95)=0s
http_req_duration..............: avg=312ms min=98ms med=280ms max=2.1s p(90)=520ms p(95)=640ms
{ expected_response:true }...: avg=312ms min=98ms med=280ms max=2.1s p(90)=520ms p(95)=640ms
http_req_failed................: 0.00% ✓ 0 ✗ 1800
http_req_receiving.............: avg=2.1ms min=45µs med=1.1ms max=74ms p(90)=4.2ms p(95)=5.8ms
http_req_sending...............: avg=178µs min=27µs med=136µs max=3.2ms p(90)=312µs p(95)=389µs
http_req_tls_handshaking.......: avg=412µs min=0s med=0s max=168ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=310ms min=97ms med=278ms max=2.1s p(90)=518ms p(95)=637ms
http_reqs......................: 1800 60.0/s
iteration_duration.............: avg=1.31s min=1.1s med=1.28s max=3.1s p(90)=1.52s p(95)=1.64s
iterations.....................: 1800 60.0/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10Les trois métriques à regarder en premier :
http_req_duration est le temps aller-retour total pour chaque requête. La colonne p(95) donne le 95e percentile : 95 % des requêtes se sont terminées dans ce délai. C'est ce chiffre qui compte pour les SLA, pas la moyenne. Les moyennes masquent les valeurs extrêmes ; le p95 non.
http_reqs indique le nombre total de requêtes effectuées et le débit par seconde.
http_req_failed montre le pourcentage de requêtes ayant reçu une réponse d'erreur (4xx ou 5xx). Zéro est attendu ; tout ce qui dépasse mérite investigation.
Ajouter des checks
La correction fonctionnelle sous charge compte autant que la vitesse. k6 dispose d'un mécanisme d'assertion appelé checks, qui fonctionne comme les instructions expect dans d'autres frameworks :
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'statut est 200': (r) => r.status === 200,
'temps de réponse < 500ms': (r) => r.timings.duration < 500,
'corps contient des produits': (r) => r.body.includes('"id"'),
});
sleep(1);
}La différence importante avec les assertions traditionnelles : un check qui échoue n'arrête pas le test. Tous les VUs continuent de tourner. k6 comptabilise combien de checks ont réussi et échoué sur toutes les itérations, et affiche le pourcentage dans le récapitulatif. C'est intentionnel. Vous voulez savoir quel pourcentage de requêtes sous charge a produit une réponse correcte, pas seulement si une requête a échoué.
Pour un endpoint de connexion qui retourne un token :
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const payload = JSON.stringify({
username: 'user@example.com',
password: 'password123',
});
const res = http.post('https://your-api.example.com/auth/login', payload, {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'connexion réussie': (r) => r.status === 200,
'token présent': (r) => JSON.parse(r.body).token !== undefined,
'connexion assez rapide': (r) => r.timings.duration < 1000,
});
sleep(1);
}Stages : montée en charge, maintien, descente
Une charge plate et constante est rarement ce dont vous avez besoin. Le trafic réel monte progressivement à l'arrivée des utilisateurs, maintient un pic, puis redescend. k6 modélise cela avec les stages, qui définissent comment le nombre de VUs évolue dans le temps :
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // montée à 50 VUs en 2 minutes
{ duration: '5m', target: 50 }, // maintien à 50 VUs pendant 5 minutes
{ duration: '2m', target: 100 }, // montée à 100 VUs en 2 minutes
{ duration: '5m', target: 100 }, // maintien à 100 VUs pendant 5 minutes
{ duration: '2m', target: 0 }, // descente à 0
],
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'statut est 200': (r) => r.status === 200,
'temps de réponse acceptable': (r) => r.timings.duration < 1000,
});
sleep(1);
}Ce profil (montée, maintien, montée, maintien, descente) correspond à un profil de test de stress. Il révèle à quel nombre d'utilisateurs votre système commence à se dégrader. Vous verrez le p95 des temps de réponse grimper régulièrement à mesure que les VUs augmentent. Le point d'inflexion où il monte brusquement est le point de rupture de votre système.
Pour la plupart des tests d'API, un test en deux phases suffit : montée puis maintien.
export const options = {
stages: [
{ duration: '1m', target: 50 }, // échauffement
{ duration: '5m', target: 50 }, // charge soutenue
{ duration: '30s', target: 0 }, // descente
],
};La phase de descente compte. Couper la charge brusquement peut masquer des problèmes de pool de connexions et donne des métriques plus propres en fin de test.
Seuils : faire passer ou échouer le test
Les checks indiquent ce qui s'est passé. Les seuils déterminent si l'exécution réussit ou échoue. Un seuil est une condition de succès/échec sur n'importe quelle métrique. S'il est violé, k6 se termine avec un code non nul. C'est ce mécanisme qui rend les tests de charge utiles en CI.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<2000'], // le 95e percentile doit rester sous 2 secondes
http_req_failed: ['rate<0.01'], // moins de 1% de requêtes peuvent échouer
checks: ['rate>0.99'], // plus de 99% des checks doivent passer
},
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'statut est 200': (r) => r.status === 200,
'temps de réponse < 2s': (r) => r.timings.duration < 2000,
});
sleep(1);
}Quand k6 exécute ce script et que le p95 dépasse 2000 ms, il se termine avec le code 99. L'étape CI échoue. C'est le comportement attendu. Un build qui rend votre API 40 % plus lente doit échouer.
Vous pouvez définir des seuils par endpoint avec des tags :
export default function () {
const loginRes = http.post(
'https://your-api.example.com/auth/login',
JSON.stringify({ username: 'user@example.com', password: 'pass' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
);
const productsRes = http.get(
'https://your-api.example.com/api/products',
{ tags: { name: 'products' } }
);
}export const options = {
thresholds: {
'http_req_duration{name:login}': ['p(95)<1000'],
'http_req_duration{name:products}': ['p(95)<500'],
},
};La connexion est naturellement plus lente qu'une liste de produits en cache, donc chaque endpoint a son propre SLA.
Lancer k6 dans GitHub Actions
k6 s'intègre proprement dans GitHub Actions. L'équipe Grafana publie une action officielle qui installe le binaire et gère la version :
# .github/workflows/load-test.yml
name: Tests de charge
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Installer k6
uses: grafana/setup-k6-action@v1
with:
k6-version: '0.55.0'
- name: Lancer le test de charge
run: k6 run load-test.js
env:
BASE_URL: ${{ vars.BASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
- name: Enregistrer les résultats k6
uses: actions/upload-artifact@v4
if: always()
with:
name: k6-results
path: k6-results.json
retention-days: 14Pour utiliser des variables d'environnement dans votre script k6, accédez-y via __ENV :
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export const options = {
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get(`${BASE_URL}/api/products`);
check(res, {
'statut est 200': (r) => r.status === 200,
'temps de réponse acceptable': (r) => r.timings.duration < 2000,
});
sleep(1);
}Pour générer un fichier JSON des résultats pour l'artifact upload, passez le flag --out :
k6 run --out json=k6-results.json load-test.jsUne recommandation pratique : ne lancez pas les tests de charge sur chaque pull request contre votre environnement de production ou de staging partagé. Le job de test de charge doit tourner sur les merges vers main, ou être déclenché manuellement via workflow_dispatch. Des tests de charge lourds sur chaque branche PR créent des interférences entre les exécutions concurrentes. Un test smoke (5 VUs, 30 secondes) sur les PR, c'est acceptable ; un test de charge complet, non.
Quels endpoints tester
Choisir quoi tester avec k6 est aussi important que le test lui-même. Tous les endpoints ne nécessitent pas un test de charge. Concentrez-vous sur ceux où la performance compte.
L'endpoint de connexion est le premier test à écrire. Chaque session utilisateur commence là. Une connexion lente multipliée par 500 utilisateurs simultanés effondre l'expérience avant même d'atteindre les fonctionnalités. Testez le flux de connexion complet : POST des credentials, réception du token, une requête authentifiée.import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<1500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
// Étape 1 : Connexion
const loginRes = http.post(
`${BASE_URL}/auth/login`,
JSON.stringify({ username: 'loadtest@example.com', password: 'loadtestpass' }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(loginRes, {
'connexion statut 200': (r) => r.status === 200,
'token retourné': (r) => JSON.parse(r.body).token !== undefined,
});
const token = JSON.parse(loginRes.body).token;
// Étape 2 : Requête authentifiée
const profileRes = http.get(`${BASE_URL}/api/me`, {
headers: { Authorization: `Bearer ${token}` },
});
check(profileRes, {
'profil statut 200': (r) => r.status === 200,
});
sleep(2);
}Un bon point de départ pour la plupart des applications : trois scripts de test de charge. Un pour l'authentification, un pour vos trois principaux endpoints de lecture dans un seul script, et un pour votre opération d'écriture critique. C'est suffisant pour détecter les régressions de performance qui comptent, sans construire une suite complète avant d'avoir une baseline.
FAQ
Ai-je besoin d'un environnement de test séparé pour lancer k6 ?Il vous faut un environnement capable d'absorber la charge sans affecter les vrais utilisateurs ni corrompre les données de production. Un environnement dédié aux tests de charge est idéal. Si vous n'avez que du staging, lancez les tests hors des heures de pointe et assurez-vous que vos données de test sont isolées. Ne pointez jamais k6 vers la production avec un nombre élevé de VUs.
Combien de VUs utiliser ?Commencez par un nombre représentant le pic de trafic réaliste, pas votre maximum théorique. Si votre application compte 50 utilisateurs simultanés un jour chargé, testez à 50-100. Savoir ce qui se passe à 1 000 VUs compte moins que de savoir que votre système gère confortablement la charge normale de pointe.
Mon test k6 passe mais l'application semble lente. Pourquoi ?k6 mesure le temps de réponse HTTP, qui inclut le traitement serveur mais exclut le rendu côté client. Un endpoint API qui répond en 200 ms peut quand même sembler lent si le frontend fait 20 appels séquentiels pour afficher une page. k6 pour les performances au niveau API ; les outils de profiling navigateur pour les performances perçues.
k6 peut-il tester des endpoints WebSocket ou gRPC ?Oui. k6 intègre le support WebSocket via le module k6/ws et gRPC via le module k6/net/grpc. Le modèle de scripting est le même ; seule l'API spécifique au protocole diffère.
Le fichier JSON généré par --out json peut être importé dans Grafana pour la visualisation. Si votre équipe utilise Grafana Cloud, k6 s'intègre nativement et envoie les résultats en temps réel pour comparaison entre les exécutions. Sans Grafana, le tableau récapitulatif terminal copié dans un commentaire de PR ou un message Slack suffit pour communiquer la réussite/échec et les percentiles clés.
Les tests de charge vérifient que votre système respecte les exigences de performance au niveau de trafic attendu. Les tests de stress poussent au-delà pour trouver le point de rupture. La configuration par stages gère les deux : un test de charge maintient un nombre cible de VUs, un test de stress continue de monter jusqu'à ce que les checks échouent.
→ See also: Fondamentaux des Tests de Performance: Load, Stress et Spike Testing | Tests d'API 101: Tout ce que Chaque Ingénieur QA Doit Savoir en 2026 | CI/CD pour QA: GitHub Actions, Jenkins et GitLab Comparés