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