Add load tests.

This commit is contained in:
Florian THIERRY
2025-10-09 14:32:00 +02:00
parent 193fcc1596
commit 13d811faae
27 changed files with 4 additions and 586 deletions

21
k6-load-tests/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Sebastian Southern
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

40
k6-load-tests/README.md Normal file
View File

@@ -0,0 +1,40 @@
# K6 Example
This repository contains a starting point for running k6 tests in typescript. It uses a publicly hosted endpoint so that anyone with internet access can test out the API themselves and then use this repo as a starting point to continue building their own tests.
## Pre-Reqs
- Download `k6` from here https://k6.io/docs/get-started/installation/
- Install dependencies `yarn install`
- Docker
### Installing xk6-dashboard with Docker
[xk6-dashboard](https://github.com/grafana/xk6-dashboard) is a k6 extension that can be used to visualise your performance test in real time.
To run the tests with monitoring with xk6-dashboard extension, we need to install it. The simplest way to install is via docker and can be done via
`docker pull ghcr.io/grafana/xk6-dashboard:0.6.1`
## Tests
### reqres
We use the [reqres](https://reqres.in/) publicly hosted REST API to showcase the testing with k6
To execute the first sample test that showcases how `per-vu-iterations` works, you can run:
`yarn test:demo`
To test with monitoring in place, run:
`yarn test-with-monitoring:demo`
To execute the second sample test that showcases how to use `stages`, you can run:
`yarn test:demo-stages`
To test with monitoring in place, run:
`yarn test-with-monitoring:demo-stages`

View File

@@ -0,0 +1,3 @@
target:
url: http://localhost
port: 51001

2208
k6-load-tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "k6-example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"scripts": {
"build": "vite build",
"test:demo": "yarn build && k6 run dist/tests/load-tests.cjs",
"test:demo-stages": "yarn build && k6 run dist/tests/reqres-stages.cjs",
"test-with-monitoring:demo": "yarn build && docker run --platform linux/amd64 -it -p 5665:5665 -v $(pwd)/dist/:/src ghcr.io/grafana/xk6-dashboard:0.6.1 run --out 'dashboard=period=2s' /src/tests/reqres.cjs",
"test-with-monitoring:demo-stages": "yarn build && docker run --platform linux/amd64 -it -p 5665:5665 -v $(pwd)/dist/:/src ghcr.io/grafana/xk6-dashboard:0.6.1 run --out 'dashboard=period=2s' /src/tests/reqres-stages.cjs"
},
"devDependencies": {
"@babel/core": "7.23.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/js-yaml": "^4.0.9",
"@types/k6": "~0.47.3",
"@types/node": "^24.5.2",
"rollup-plugin-copy": "^3.5.0",
"typescript": "5.3.2",
"vite": "^5.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"js-yaml": "^4.1.0"
}
}

View File

@@ -0,0 +1,33 @@
import {Marketplace} from "../data/marketplace";
import http from "k6/http";
import {logWaitingTime} from "../utils/logger";
import {Trend} from "k6/metrics";
import {check} from "k6";
import {Response} from "../data/common";
// Metrics that we want to track
const metrics = {
getMarketplace: new Trend("get_marketplace_time", true),
};
export const getMarketplace = (): Response<Marketplace> => {
const serverUrl = `http://localhost:51001`;
const urlToTest = `${serverUrl}/api/marketplace`;
const response = http.get(urlToTest);
logWaitingTime({
metric: metrics.getMarketplace,
response: response,
messageType: 'Get Marketplace',
});
check(response, {
'Get Marketplace: is 200': r => r.status === 200
});
return {
data: {} as Marketplace,
statusCode: response.status
}
}

View File

@@ -0,0 +1,4 @@
export type Response<T> = {
data: T;
statusCode: number;
};

View File

@@ -0,0 +1,15 @@
export interface Catalog {
id: string;
name: string;
}
export interface Item {
id: string;
name: string;
isShared: boolean;
}
export interface Marketplace {
catalogs: Catalog[];
sharedItems: Item[]
}

View File

@@ -0,0 +1 @@
export const VUs = 1

View File

@@ -0,0 +1,22 @@
// @ts-ignore
import { vu } from "k6/execution";
import { Options } from "k6/options";
import { logger } from "../utils/logger";
import {getMarketplace} from "../apis/marketplace-apis";
export const options: Options = {
scenarios: {
login: {
executor: 'per-vu-iterations',
vus: 50,
iterations: 10,
maxDuration: '1h30m',
},
},
};
export default function test () {
logger.info(`Running iteration ${vu.iterationInInstance}`);
getMarketplace();
}

View File

@@ -0,0 +1,49 @@
export const getTimestamp = (): string => {
const date = new Date();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
const milliseconds = date.getMilliseconds().toString().padStart(3, "0");
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
};
export const logger = {
info(...val: any): void {
console.log(getTimestamp(), ...val);
},
warn(...val: any): void {
console.warn(getTimestamp(), ...val);
},
error(...val: any): void {
console.error(getTimestamp(), ...val);
},
};
export const logWaitingTime = ({
metric,
response,
messageType,
}: {
metric: any;
response: any;
messageType: string;
}): void => {
const responseTimeThreshold = 5000;
let correlationId = "";
let responseTime = response.timings.waiting;
try {
let json = response.json();
correlationId = json.correlationId;
} catch (err) {
// noop
}
// Log any responses that far longer than expected so we can troubleshoot those particular queries
if (responseTime > responseTimeThreshold) {
logger.warn(
`${messageType} with correlationId '${correlationId}' took longer than ${responseTimeThreshold}`
);
}
metric.add(responseTime);
};

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
"module": "commonjs",
"noEmit": true,
"allowJs": true,
"removeComments": false,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
}
}

View File

@@ -0,0 +1,67 @@
import { defineConfig } from 'vite';
import { babel } from '@rollup/plugin-babel';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import copy from 'rollup-plugin-copy';
import fg from 'fast-glob';
/**
* Gets the entrypoints of files within a set of given paths (i.e src/tests).
* This is useful as when we execute k6, we run it against individual test files
* @returns an object of [fileName]: absolute file path
*/
const getEntryPoints = (entryPoints) => {
// Searches for files that match the patterns defined in the array of input points.
// Returns an array of absolute file paths.
const files = fg.sync(entryPoints, { absolute: true });
// Maps the file paths in the "files" array to an array of key-value pair.
const entities = files.map((file) => {
// Extract the part of the file path after the "src" folder and before the file extension.
const [key] = file.match(/(?<=src\/).*$/) || [];
// Remove the file extension from the key.
const keyWithoutExt = key.replace(/\.[^.]*$/, '');
return [keyWithoutExt, file];
});
// Convert the array of key-value pairs to an object using the Object.fromEntries() method.
// Returns an object where each key is the file name without the extension and the value is the absolute file path.
return Object.fromEntries(entities);
};
export default defineConfig({
mode: 'production',
build: {
lib: {
entry: getEntryPoints(['./src/tests/*.ts']),
fileName: '[name]',
formats: ['cjs'],
sourcemap: true,
},
outDir: 'dist',
minify: false, // Don't minimize, as it's not used in the browser
rollupOptions: {
external: [new RegExp(/^(k6|https?\:\/\/)(\/.*)?/)],
},
},
resolve: {
extensions: ['.ts', '.js'],
},
plugins: [
copy({
targets: [
{
src: 'assets/**/*',
dest: 'dist',
noErrorOnMissing: true,
},
],
}),
babel({
babelHelpers: 'bundled',
exclude: /node_modules/,
}),
nodeResolve(),
],
});

1160
k6-load-tests/yarn.lock Normal file

File diff suppressed because it is too large Load Diff