→ Il sert à identifier les goulets d'étranglement, garantir une bonne expérience utilisateur et assurer la scalabilité avant la mise en production
Le script se trouve dans load-test/flashcards.js, vous pouvez le visualiser en entier dans l'application Flashcards
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 10 },
{ duration: '1m', target: 30 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8081';
http_req_failed: ['rate < 0.01']: défini à moins de 1% d'erreurs HTTP, sinon le test est considéré comme en échec
http_req_duration: ['p(95) < 500']: 95% des requêtes doivent répondre en moins de 500 ms, sinon c'est un échec de performance
export default function () {
const params = {
headers: {
'Content-Type': 'application/json',
},
};
const categoryPayload = JSON.stringify({
name: `cat-${Math.random()}`,
});
const resCat = http.post(
`${BASE_URL}/api/categories`,
categoryPayload,
params
);
check(resCat, {
'create category OK': (r) => r.status === 200 || r.status === 201,
});
if (!(resCat.status === 200 || resCat.status === 201)) {
return;
}
let catId;
try {
catId = resCat.json('id');
} catch (e) {
console.log(`Category JSON parse error: ${e}`);
return;
}
const flashcardPayload = JSON.stringify({
categoryId: catId,
front: 'question test k6',
back: 'réponse test k6',
});
sleep(0.05);
const resFlash = http.post(
`${BASE_URL}/api/flashcards`,
flashcardPayload,
params
);
check(resFlash, {
'create flashcard OK': (r) => r.status === 200 || r.status === 201,
});
// READ OPERATIONS
const resGetCats = http.get(`${BASE_URL}/api/categories`);
check(resGetCats, {
'get categories OK': (r) => r.status === 200,
});
const resGetFlashcards = http.get(`${BASE_URL}/api/flashcards`);
check(resGetFlashcards, {
'get flashcards OK': (r) => r.status === 200,
});
sleep(1);
}
k6 tourne dans le même pipeline que les tests fonctionnels ce qui donne une vue "qualité + performance"
- name: Setup k6
uses: grafana/setup-k6-action@v1.1.0
- name: Run k6 LOCAL (staging)
env:
BASE_URL: http://localhost:8081
run: k6 run load-test/flashcards.js
--vus 50 --duration 2m --out json=k6-report.json
Le job load-test tourne sur ubuntu-24.04
Il s'exécute après les tests fonctionnels (needs: functional-tests)
et, uniquement sur push pour éviter de surcharger les PR
Il utilise un environnement staging réel: Spring Boot + Postgres et profil "staging"
Il réutilise l'artifact, téléchargé dans le job précédent ("functional-tests") pour faire tourner les tests dedans
Les logs applicatifs sont uploadés en artifact, ce qui facilite l'analyse post-mortem en cas de problème
Output du job dans les Actions Github: Workflow "staging" et regarder le job "load-test"
Run k6 run load-test/flashcards.js --vus 50 --duration 2m --out json=k6-report.json
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: load-test/flashcards.js
output: json (k6-report.json)
scenarios: (100.00%) 1 scenario, 50 max VUs, 2m30s max duration (incl. graceful stop):
* default: 50 looping VUs for 2m0s (gracefulStop: 30s)
running (0m01.0s), 50/50 VUs, 0 complete and 0 interrupted iterations
default [ 1% ] 50 VUs 0m01.0s/2m0s
running (0m02.0s), 50/50 VUs, 0 complete and 0 interrupted iterations
default [ 2% ] 50 VUs 0m02.0s/2m0s
//...
running (2m01.0s), 05/50 VUs, 5524 complete and 0 interrupted iterations
default ↓ [ 100% ] 50 VUs 2m0s
█ THRESHOLDS
http_req_duration
✓ 'p(95)<500' p(95)=21.01ms
http_req_failed
✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_total.......: 22116 182.666242/s
checks_succeeded...: 100.00% 22116 out of 22116
checks_failed......: 0.00% 0 out of 22116
✓ create category OK
✓ create flashcard OK
✓ get categories OK
✓ get flashcards OK
HTTP
http_req_duration..............: avg=9.53ms min=851.92µs med=4.53ms max=448.36ms p(90)=16.28ms p(95)=21.01ms
{ expected_response:true }...: avg=9.53ms min=851.92µs med=4.53ms max=448.36ms p(90)=16.28ms p(95)=21.01ms
http_req_failed................: 0.00% 0 out of 22116
http_reqs......................: 22116 182.666242/s
EXECUTION
iteration_duration.............: avg=1.08s min=1.05s med=1.07s max=2.11s p(90)=1.09s p(95)=1.1s
iterations.....................: 5529 45.66656/s
vus............................: 4 min=4 max=50
vus_max........................: 50 min=50 max=50
NETWORK
data_received..................: 1.6 GB 13 MB/s
data_sent......................: 3.0 MB 25 kB/s
running (2m01.1s), 00/50 VUs, 5529 complete and 0 interrupted iterations
default ✓ [ 100% ] 50 VUs 2m0s

Pour un petit service CRUD Spring Boot, c'est déjà une charge significative
| Métrique | Valeur typique |
|---|---|
| Médiane (p50) | ~4-5 ms |
| p90 | ~16 ms |
| p95 | ~21 ms |
| Max | ~400-450 ms |
On a, à présent, un test de charge automatisé sur la branche staging
ce qui prouve que l'API supporte la création concurrente de catégories et de flashcards à ~180 req/s, tout en restant sous 500 ms pour 95% des réponses