feat: Added k6 performance tests
This commit is contained in:
@@ -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
|
||||||
@@ -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'],
|
||||||
|
};
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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) });
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user