feat: Added k6 performance tests

This commit is contained in:
2026-05-10 22:00:24 +02:00
commit 5f7dd2e281
9 changed files with 290 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
# K6 Load Tests
Load tests for NowChess using K6 framework.
## Installation
```bash
# Install K6 - https://k6.io/docs/getting-started/installation/
# macOS
brew install k6
# Ubuntu/Debian
sudo apt-get install k6
# Windows (Chocolatey)
choco install k6
```
## Running Tests
### Smoke Test
One user, basic journey validation.
```bash
k6 run simulations/smokeTest.js
```
### Load Test
Ramp up to max users over duration.
```bash
BASE_URL=http://localhost:8080 MAX_USERS=5 RAMP_DURATION=60 k6 run simulations/loadTest.js
```
### Stress Test
Incrementally increase users in steps.
```bash
BASE_URL=http://localhost:8080 \
START_USERS=2 \
USERS_INCREMENT=2 \
STEPS=2 \
STEP_DURATION=30 \
RAMP_DURATION=10 \
k6 run simulations/stressTest.js
```
### Spike Test
Baseline → spike → baseline.
```bash
BASE_URL=http://localhost:8080 \
BASELINE_USERS=2 \
BASELINE_DURATION=20 \
SPIKE_USERS=15 \
k6 run simulations/spikeTest.js
```
### Endurance Test
Constant load over long duration.
```bash
BASE_URL=http://localhost:8080 \
CONCURRENT_USERS=3 \
DURATION=300 \
k6 run simulations/enduranceTest.js
```
## Environment Variables
- `BASE_URL` - API base URL (default: `http://localhost:8080`)
- `AUTH_TOKEN` - Optional Bearer token for global auth
- Test-specific variables documented in each simulation file
## Output
Results summary printed to console. For detailed metrics, use K6 output options:
```bash
# JSON output
k6 run --out json=results.json simulations/smokeTest.js
# Cloud output (requires K6 Cloud account)
k6 run --out cloud simulations/smokeTest.js
# InfluxDB + Grafana
k6 run --out influxdb simulations/smokeTest.js
```
## Structure
- `config.js` - Shared configuration, headers, checks
- `scenarios/` - Reusable scenario definitions
- `simulations/` - Test simulations with load profiles
+23
View File
@@ -0,0 +1,23 @@
export const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
export const AUTH_TOKEN = __ENV.AUTH_TOKEN || '';
export const httpConfig = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...(AUTH_TOKEN && { 'Authorization': `Bearer ${AUTH_TOKEN}` }),
},
timeout: '30s',
};
export const checks = {
statusOk: (res) => res.status === 200,
statusOkOrCreated: (res) => res.status === 200 || res.status === 201,
statusOkCreatedOrNoContent: (res) => [200, 201, 204].includes(res.status),
responseTimeUnder5s: (res) => res.timings.duration < 5000,
};
export const thresholds = {
'http_req_duration': ['p(95)<5000', 'p(99)<10000'],
'http_req_failed': ['rate<0.1'],
};
+13
View File
@@ -0,0 +1,13 @@
{
"name": "k6-tests",
"version": "1.0.0",
"description": "K6 load tests for NowChess",
"scripts": {
"test:smoke": "k6 run simulations/smokeTest.js",
"test:load": "k6 run simulations/loadTest.js",
"test:stress": "k6 run simulations/stressTest.js",
"test:spike": "k6 run simulations/spikeTest.js",
"test:endurance": "k6 run simulations/enduranceTest.js"
},
"keywords": ["k6", "load-testing", "performance"]
}
+67
View File
@@ -0,0 +1,67 @@
import http from 'k6/http';
import { check } from 'k6';
import { BASE_URL, httpConfig } from '../config.js';
export function chessUserJourney() {
const username = `user_${Date.now()}_${__VU}_${Math.random().toString(36).substring(2, 10)}`;
const email = `${username}@test.com`;
const password = 'Password123!';
// Register
const registerRes = http.post(`${BASE_URL}/api/account`, JSON.stringify({
username,
email,
password,
}), httpConfig);
check(registerRes, { 'Register: status 200': (r) => r.status === 200 });
// Login
const loginRes = http.post(`${BASE_URL}/api/account/login`, JSON.stringify({
username,
password,
}), httpConfig);
check(loginRes, { 'Login: status 200': (r) => r.status === 200 });
const jwt = loginRes.json('token');
if (!jwt) {
console.error('Failed to get JWT token');
return;
}
const authHeaders = {
...httpConfig.headers,
'Authorization': `Bearer ${jwt}`,
};
// Import Game
const importRes = http.post(`${BASE_URL}/api/board/game/import/fen`, JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
white: { id: username, displayName: username },
black: { id: `opponent_${__VU}`, displayName: 'Opponent' },
timeControl: { limitSeconds: 300, incrementSeconds: 3 },
}), { headers: authHeaders, timeout: '30s' });
check(importRes, { 'Import Game: status 200/201': (r) => r.status === 200 || r.status === 201 });
const gameId = importRes.json('gameId');
if (!gameId) {
console.error('Failed to get gameId');
return;
}
// Make moves
const moves = ['e2e4', 'e7e5', 'g1f3'];
for (const uci of moves) {
const moveRes = http.post(`${BASE_URL}/api/board/game/${gameId}/move/${uci}`, null, {
headers: authHeaders,
timeout: '30s',
});
check(moveRes, { [`Move ${uci}: status 200/201`]: (r) => r.status === 200 || r.status === 201 });
}
// Resign
const resignRes = http.post(`${BASE_URL}/api/board/game/${gameId}/resign`, null, {
headers: authHeaders,
timeout: '30s',
});
check(resignRes, { 'Resign: status 200/201/204': (r) => [200, 201, 204].includes(r.status) });
}
+15
View File
@@ -0,0 +1,15 @@
import { chessUserJourney } from '../scenarios/chessUserScenario.js';
import { thresholds } from '../config.js';
const CONCURRENT_USERS = parseInt(__ENV.CONCURRENT_USERS || '3', 10);
const DURATION = parseInt(__ENV.DURATION || '300', 10);
export const options = {
vus: CONCURRENT_USERS,
duration: `${DURATION}s`,
thresholds,
};
export default function () {
chessUserJourney();
}
+16
View File
@@ -0,0 +1,16 @@
import { chessUserJourney } from '../scenarios/chessUserScenario.js';
import { thresholds } from '../config.js';
const MAX_USERS = parseInt(__ENV.MAX_USERS || '5', 10);
const RAMP_DURATION = parseInt(__ENV.RAMP_DURATION || '60', 10);
export const options = {
stages: [
{ duration: `${RAMP_DURATION}s`, target: MAX_USERS },
],
thresholds,
};
export default function () {
chessUserJourney();
}
+12
View File
@@ -0,0 +1,12 @@
import { chessUserJourney } from '../scenarios/chessUserScenario.js';
import { thresholds } from '../config.js';
export const options = {
vus: 1,
duration: '1m',
thresholds,
};
export default function () {
chessUserJourney();
}
+21
View File
@@ -0,0 +1,21 @@
import { chessUserJourney } from '../scenarios/chessUserScenario.js';
import { thresholds } from '../config.js';
const BASELINE_USERS = parseInt(__ENV.BASELINE_USERS || '2', 10);
const BASELINE_DURATION = parseInt(__ENV.BASELINE_DURATION || '20', 10);
const SPIKE_USERS = parseInt(__ENV.SPIKE_USERS || '15', 10);
export const options = {
stages: [
{ duration: `${BASELINE_DURATION}s`, target: BASELINE_USERS },
{ duration: '1s', target: BASELINE_USERS + SPIKE_USERS },
{ duration: '5s', target: BASELINE_USERS + SPIKE_USERS },
{ duration: '1s', target: BASELINE_USERS },
{ duration: `${BASELINE_DURATION}s`, target: BASELINE_USERS },
],
thresholds,
};
export default function () {
chessUserJourney();
}
+34
View File
@@ -0,0 +1,34 @@
import { chessUserJourney } from '../scenarios/chessUserScenario.js';
import { thresholds } from '../config.js';
const START_USERS = parseInt(__ENV.START_USERS || '2', 10);
const USERS_INCREMENT = parseInt(__ENV.USERS_INCREMENT || '2', 10);
const STEPS = parseInt(__ENV.STEPS || '2', 10);
const STEP_DURATION = parseInt(__ENV.STEP_DURATION || '30', 10);
const RAMP_DURATION = parseInt(__ENV.RAMP_DURATION || '10', 10);
const stages = [];
let currentUsers = START_USERS;
for (let i = 0; i < STEPS; i++) {
// Ramp up
stages.push({
duration: `${RAMP_DURATION}s`,
target: currentUsers,
});
// Hold at level
stages.push({
duration: `${STEP_DURATION}s`,
target: currentUsers,
});
currentUsers += USERS_INCREMENT;
}
export const options = {
stages,
thresholds,
};
export default function () {
chessUserJourney();
}