├── .babelrc ├── .gitignore ├── README.md ├── docker-compose.yml ├── grafana-datasource.yaml ├── images └── FrameworkDiagram.png ├── package.json ├── src ├── actions │ ├── crocodile-management.actions.ts │ ├── roles │ │ ├── admin.role.ts │ │ ├── crocodile-owner.role.ts │ │ └── public-user.role.ts │ ├── session-management.actions.ts │ └── user-management.actions.ts ├── data │ └── crocodiles.json ├── lib │ ├── request.helpers.ts │ ├── sleep.helpers.ts │ ├── test-data.helpers.ts │ └── types │ │ ├── crocodile.api.d.ts │ │ └── framework.types.d.ts └── tests │ ├── create-crocs.seed.ts │ └── soak.test.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "presets": [ 4 | 5 | [ 6 | 7 | "@babel/typescript" 8 | 9 | ] 10 | 11 | ], 12 | 13 | "plugins": [ 14 | 15 | "@babel/proposal-class-properties", 16 | 17 | "@babel/proposal-object-rest-spread" 18 | 19 | ] 20 | 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k6 Typescript Framework 2 | A starter framework for k6 load tests written in TypeScript. 3 | 4 | We'll be using the [LoadImpact Test API](https://test-api.loadimpact.com/) as the website we'll be testing. This is a dummy application/api for crocodiles owners to use who want to keep track of their crocodiles. In the test we will **create a user**, **query** some crocodiles, and **create**, **update** and **delete** a crocodile. 5 | 6 | ## Quick Start :zap: 7 | 8 | Install the [k6 performance test tool](https://docs.k6.io/docs/installation). 9 | 10 | Clone this repository and open in the IDE of your choice. 11 | 12 | Install dependencies using: 13 | 14 | `yarn install` 15 | 16 | in the terminal (you need to have [yarn](https://yarnpkg.com/getting-started/install) installed on your machine). 17 | 18 | Now run the test using the following command: 19 | 20 | `yarn go:k6` 21 | 22 | This will run the [soak.test.ts](/src/tests/soak.test.ts) script, using **k6**. 23 | 24 | ## Run with Monitoring 25 | 26 | ![Grafana Dashboard](https://grafana.com/api/dashboards/11837/images/7658/image) 27 | 28 | Ensure you have [docker](https://www.docker.com/products/docker-desktop) and [docker-compose](https://docs.docker.com/compose/install/) installed on your machine. 29 | 30 | Start the monitors using the following command: 31 | 32 | `yarn monitors` 33 | 34 | Go to **localhost:3000** in your browser to login to Grafana with the username '**admin**' and the password '**admin**'. 35 | 36 | Add the [k6 dashboard](https://grafana.com/grafana/dashboards/11837) to **Grafana** by following these instructions: [Importing a Dashboad](https://grafana.com/docs/grafana/latest/reference/export_import/) 37 | 38 | Now run the test using the following command: 39 | 40 | `yarn go:docker` 41 | 42 | This will run the [soak.test.ts](/src/tests/soak.test.ts) script, using **k6** installed in a docker, which outputs the results to **influxDB**. **Grafana** is used to visualise the results. 43 | 44 | **Please NOTE:** If you're running in **Windows** you'll need to use the full path for the local directories in the **volumes** sections of the [docker-compose.yaml](docker-compose.yml) file. See the [k6 documentation](https://docs.k6.io/docs/docker-on-windows) for more details. 45 | 46 | 47 | ## Run the 'Seed' Script 48 | 49 | [create-crocs.seed.ts](src/tests/create-crocs.seed.ts) 50 | 51 | This is an example of a script that you could use to 'seed' the application with test data before you run your performance tests. You can run it using the following command: 52 | 53 | `yarn seed`. 54 | 55 | This is just an example script and not needed for the test. 56 | 57 | ## The Test Framework :white_check_mark: 58 | 59 | The test is based on the following sample script and API provided by k6: 60 | 61 | https://test-api.loadimpact.com/ 62 | 63 | This is a dummy api for people who own crocodiles to keep track of their crocodiles. 64 | 65 | ![Crocodile Pic](https://images.pexels.com/photos/207001/pexels-photo-207001.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260, "Photo by Pixabay from Pexels") 66 | 67 | In the test we will **create** a user, **query** crocodiles, and **create**, **update** and **delete** a crocodile. The test also include [thresholds](https://docs.k6.io/docs/thresholds) and [checks](https://docs.k6.io/docs/checks) 68 | 69 | I've converted the test to TypeScript and broken it out into modules so it's easier to use and scale. 70 | 71 | Here's the high-level architecture diagram for the framework: 72 | 73 | ![Framework Diagram](images/FrameworkDiagram.png) 74 | 75 | As you can see from the framework diagram above, k6 modules allow for a lot of code re-use. Let's go into more detail about each of the folders and what they do. 76 | 77 | ### **src** folder 78 | 79 | All the code can be found in the `src` folder. And is written in TypeScript using [types provided by k6](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/k6). 80 | 81 | ### **lib** folder 82 | 83 | This folder contains bespoke `types` and helper functions. It's highly recommended that you unit test your helper functions (e.g. with [Jest](https://jestjs.io/)). However I've not done that here, just to keep things simple. 84 | 85 | #### The types folder 86 | 87 | This currently contains an interface for a 'User' in the system, specifying that they need a first name, last name, username and password. 88 | 89 | ### **actions** folder 90 | 91 | The `actions` folder contains a script file for each user action. Each script file contains the requests that are sent when a user performs that particular action (e.g. login). The `roles` folder (inside the `actions` folder) contains a file for each user type and the actions they can perform. 92 | 93 | #### *roles folder* 94 | 95 | There are three types of user (or roles) that use the Crocodile app. The first is a *public user* that isn't logged into the system. They can query crocodiles, but can't create, update or delete them. The second are *crocodile owners* who can log in and create, read and update crocodiles. The third are *admin* users who can create other users. The admin users don't need to log in, as this is just a dummy app. 96 | 97 | ### **tests** folder 98 | 99 | This is where you create your performance tests using the modules from the rest of the framework. `actions` are never called directly, but always through the `role` performing them (see the `actions` and `roles` folders above). 100 | 101 | ## Checking your Code 102 | 103 | Use: 104 | 105 | `yarn check-types` 106 | 107 | to check your code against type safety and the rules set in your [tsconfig.json file](tsconfig.json). You can also have this running while you work using: 108 | 109 | `yarn check-types:watch`. 110 | 111 | **PLEASE NOTE** I haven't set up `ESLint` and `Prettier` which this framework, but it's recommended that you do so. 112 | 113 | ## Building your Code 114 | 115 | [Babel](https://babeljs.io/) handles the transpiling of the code (see the [.babelrc](.babelrc) file in the root directory), while [Webpack](https://webpack.js.org/) builds it (see the [webpack.config.js](webpack.config.js) file in the root directory). 116 | 117 | ## Debugging k6 :bug: 118 | 119 | It's easy to debug `k6` scripts. See the [k6 documentation](https://docs.k6.io/docs/debugging) for more details. 120 | 121 | ## Running in CI/CD Pipelines 122 | 123 | `k6` has been designed to work with your `CI/CD` pipeline whatever tool you're using. There are examples for [GitHub Actions](https://blog.loadimpact.com/load-testing-using-github-actions), [GitLab](https://blog.loadimpact.com/integrating-load-testing-with-gitlab), [CircleCI](https://github.com/loadimpact/k6-circleci-example), [Jenkins](https://github.com/loadimpact/k6-jenkins-example) and many others. 124 | 125 | 126 | ## Problems with this Framework 127 | 128 | If you notice any problems or improvements that could be made to this example framework, I accept PRs or you can raise an issue on the [k6 community forum](https://community.k6.io/) 129 | 130 | ## TO DO 131 | 132 | I can't for the life of me get this framework working with k6's [base compatibility mode](https://github.com/MStoykov/k6-es6) for optimum performance. I'm putting this down to my lack of TypeScript and Webpack/Babel knowledge, so if anyone can solve this, please let me know or raise a PR! -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | networks: 4 | k6: 5 | grafana: 6 | 7 | services: 8 | influxdb: 9 | image: influxdb:latest 10 | networks: 11 | - k6 12 | - grafana 13 | ports: 14 | - "8086:8086" 15 | environment: 16 | - INFLUXDB_DB=k6 17 | 18 | grafana: 19 | image: grafana/grafana:latest 20 | networks: 21 | - grafana 22 | ports: 23 | - "3000:3000" 24 | environment: 25 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 26 | - GF_AUTH_ANONYMOUS_ENABLED=true 27 | - GF_AUTH_BASIC_ENABLED=false 28 | volumes: 29 | - ./grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yaml 30 | 31 | k6: 32 | image: loadimpact/k6:latest 33 | networks: 34 | - k6 35 | ports: 36 | - "6565:6565" 37 | environment: 38 | - K6_OUT=influxdb=http://influxdb:8086/k6 39 | volumes: 40 | - ./dist:/scripts 41 | -------------------------------------------------------------------------------- /grafana-datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: myinfluxdb 5 | type: influxdb 6 | access: proxy 7 | database: k6 8 | url: http://influxdb:8086 9 | isDefault: true 10 | -------------------------------------------------------------------------------- /images/FrameworkDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-automate/k6-typescript-framework/6aa19357824f89485b68b51b0605d6de8b6ecde3/images/FrameworkDiagram.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k6-typescript-framework", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:go-automate/k6-typescript-framework.git", 6 | "author": "Simon Stratton ", 7 | "license": "MIT", 8 | "scripts": { 9 | "check-types": "tsc", 10 | "check-types:watch": "tsc --watch", 11 | "monitors": "docker-compose up -d influxdb grafana", 12 | "build": "webpack", 13 | "test": "docker-compose run --rm k6 run /scripts/soakTest.bundle.js", 14 | "seed": "docker-compose run --rm k6 run /scripts/seedCrocs.bundle.js", 15 | "go:docker": "yarn build && yarn test", 16 | "go:k6": "yarn build && k6 run dist/soakTest.bundle.js" 17 | }, 18 | "dependencies": { 19 | "@types/k6": "^0.25.1", 20 | "@types/node": "^13.7.1" 21 | }, 22 | "devDependencies": { 23 | "@babel/cli": "^7.8.4", 24 | "@babel/core": "^7.8.4", 25 | "@babel/node": "^7.8.4", 26 | "@babel/plugin-proposal-class-properties": "^7.8.3", 27 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 28 | "@babel/preset-env": "^7.8.4", 29 | "@babel/preset-typescript": "^7.8.3", 30 | "babel-loader": "^8.0.6", 31 | "eslint": "^6.8.0", 32 | "eslint-config-prettier": "^6.10.0", 33 | "eslint-plugin-prettier": "^3.1.2", 34 | "prettier": "^1.19.1", 35 | "typescript": "^3.7.5", 36 | "webpack": "^4.41.6", 37 | "webpack-cli": "^3.3.11" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/actions/crocodile-management.actions.ts: -------------------------------------------------------------------------------- 1 | import { group, check, fail } from "k6"; 2 | import http from "k6/http" 3 | 4 | import { randomCrocodile } from '../lib/test-data.helpers' 5 | import { setSleep } from "../lib/sleep.helpers"; 6 | import { Crocodile } from "../lib/types/crocodile.api"; 7 | import { Counter } from "k6/metrics"; 8 | 9 | export function createCrocodile( _requestConfigWithTag: any, _url: string, count: Counter ): string { 10 | 11 | group('Create Crocs', () => { 12 | 13 | // body of the post request 14 | const payload: Crocodile = randomCrocodile(); 15 | 16 | const res = http.post(_url, payload as {}, _requestConfigWithTag({ name: 'Create' })); 17 | 18 | const createdCroc: Crocodile = JSON.parse(res.body as string); 19 | 20 | // check the crock is created 21 | if (check(res, { 'Croc created correctly': (r) => r.status === 201 })) { 22 | _url = `${_url}${createdCroc.id}/`; 23 | count.add(1); 24 | } else { 25 | fail(`Unable to create a Croc ${res.status} ${res.body}`); 26 | } 27 | 28 | setSleep(0.5, 1); 29 | 30 | }); 31 | 32 | return _url 33 | } 34 | 35 | export function updateCrocodile( _requestConfigWithTag: any, _url: string, _newName: string, count: Counter ){ 36 | group('Update Croc', () => { 37 | const payload = { name: `${_newName}` }; 38 | const res = http.patch(_url, payload, _requestConfigWithTag({ name: 'Update' })); 39 | const isSuccessfulUpdate = check(res, { 40 | 'Update worked': () => res.status === 200, 41 | 'Updated name is correct': () => res.json('name') === 'New Name', 42 | }); 43 | 44 | // if checks failed, log this. 45 | if (!isSuccessfulUpdate) { 46 | console.log(`Unable to update the croc ${res.status} ${res.body}`); 47 | return 48 | } 49 | count.add(1); 50 | }); 51 | 52 | setSleep(0.5, 1); 53 | } 54 | 55 | export function deleteCrocodile( _requestConfigWithTag: any, _url: string, count: Counter ){ 56 | 57 | group('Delete Croc', () => { 58 | const delRes = http.del(_url, null, _requestConfigWithTag({ name: 'Delete' })); 59 | const isSuccessfulDelete = check(null, { 60 | 'Croc was deleted correctly': () => delRes.status === 204, 61 | }); 62 | if (!isSuccessfulDelete) { 63 | console.log(`Croc was not deleted properly`); 64 | return 65 | } 66 | count.add(1); 67 | }) 68 | 69 | setSleep(0.5, 1); 70 | 71 | } 72 | 73 | export function queryCrocodiles(_url: string, crocs:Crocodile[]): Crocodile[]{ 74 | 75 | // these dont need auth as they're public endpoints - all tagged with the same name and therefore will be tested by the same threshold 76 | group('Query Crocodiles', () => { 77 | // call some public endpoints in a batch request https://docs.k6.io/docs/batch-requests 78 | let responses = http.batch([ 79 | ['GET', `${_url}/public/crocodiles/1/`, null, {tags: {name: 'PublicCrocs'}}], 80 | ['GET', `${_url}/public/crocodiles/2/`, null, {tags: {name: 'PublicCrocs'}}], 81 | ['GET', `${_url}/public/crocodiles/3/`, null, {tags: {name: 'PublicCrocs'}}], 82 | ['GET', `${_url}/public/crocodiles/4/`, null, {tags: {name: 'PublicCrocs'}}], 83 | ]); 84 | 85 | for(let i=0; i < responses.length; i++){ 86 | check(responses[i], { 87 | 'Query successful': () => responses[i].status === 200 88 | }); 89 | crocs.push(JSON.parse(responses[i].body as string)) 90 | } 91 | 92 | }); 93 | 94 | setSleep(0.5, 1); 95 | 96 | return crocs; 97 | } 98 | 99 | export function checkAges(_crocs: Crocodile[], _minAge: number ){ 100 | 101 | group('Functional Test: Check Ages', () => { 102 | // get all the ages out of each of the responses 103 | const ages = Object.values(_crocs).map(croc => croc.age); 104 | 105 | // Functional test: check that all the public crocodiles are older than 5 106 | check(ages as number[], { 107 | 'Crocs are older than 5 years of age': (ages) => Math.min(...ages) > _minAge 108 | }); 109 | }) 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/actions/roles/admin.role.ts: -------------------------------------------------------------------------------- 1 | export { registerNewUser } from '../user-management.actions' 2 | -------------------------------------------------------------------------------- /src/actions/roles/crocodile-owner.role.ts: -------------------------------------------------------------------------------- 1 | export { login } from '../session-management.actions' 2 | export { createCrocodile, updateCrocodile, deleteCrocodile } from '../crocodile-management.actions' -------------------------------------------------------------------------------- /src/actions/roles/public-user.role.ts: -------------------------------------------------------------------------------- 1 | export { queryCrocodiles, checkAges } from '../crocodile-management.actions' -------------------------------------------------------------------------------- /src/actions/session-management.actions.ts: -------------------------------------------------------------------------------- 1 | import http, { RefinedResponse } from "k6/http"; 2 | import { check, JSONValue } from "k6"; 3 | 4 | import { User } from "../lib/types/framework.types"; 5 | import { LoginResponseBody } from "../lib/types/crocodile.api" 6 | 7 | import { setSleep } from "../lib/sleep.helpers"; 8 | 9 | export function login(_user: User, _url: string): JSONValue { 10 | 11 | // login using a 'post' request and store the result in 'loginRes' 12 | let loginRes: RefinedResponse<"text"> = http.post(`${_url}/auth/token/login/`, { 13 | username: _user.username, 14 | password: _user.password 15 | }); 16 | 17 | const loginData: LoginResponseBody = JSON.parse(loginRes.body); 18 | 19 | // parse the 'loginRes' response as 'JSON' and use the 'access' selector to grab the Bearer token https://docs.k6.io/docs/response-k6http 20 | let authToken = loginData.access; 21 | 22 | // check that the token is not empty 23 | check(authToken, { 'logged in successfully': () => authToken !== '', }); 24 | 25 | setSleep(0.5, 1); 26 | 27 | return authToken; 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/actions/user-management.actions.ts: -------------------------------------------------------------------------------- 1 | import http, { RefinedResponse, RefinedBatchRequest, StructuredRequestBody } from 'k6/http'; 2 | import {check, group, sleep, fail} from 'k6'; 3 | import { Options } from 'k6/options'; 4 | 5 | import { randomString } from '../lib/test-data.helpers' 6 | 7 | import { User } from '../lib/types/framework.types' 8 | import { setSleep } from '../lib/sleep.helpers'; 9 | 10 | export function registerNewUser(_user: User, _url: string){ 11 | group('Register a New User', () => { 12 | // register a new user using a 'post' request https://docs.k6.io/docs/post-url-body-params and store the response in 'res' 13 | let res = http.post(`${_url}/user/register/`, _user as {}); 14 | 15 | // check that the user is created (that the response is '201') https://docs.k6.io/docs/checks 16 | check(res, { 'created user': (r) => r.status === 201 }); 17 | }) 18 | 19 | setSleep(0.5, 1); 20 | } -------------------------------------------------------------------------------- /src/data/crocodiles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "croc1", 4 | "sex": "male", 5 | "date_of_birth": "2001-01-01", 6 | "age": 2 7 | }, 8 | { 9 | "name": "croc2", 10 | "sex": "male", 11 | "date_of_birth": "2001-01-01", 12 | "age": 5 13 | }, 14 | { 15 | "name": "croc3", 16 | "sex": "male", 17 | "date_of_birth": "2001-01-01", 18 | "age": 3 19 | } 20 | ] -------------------------------------------------------------------------------- /src/lib/request.helpers.ts: -------------------------------------------------------------------------------- 1 | import { JSONValue } from "k6"; 2 | 3 | export function createRequestConfigWithTag(_authToken: JSONValue){ 4 | // the headers and tags needed for logged-in request https://docs.k6.io/docs/http-requests 5 | return (tag:{ [key: string]: string; }) => ({ 6 | headers: { 7 | Authorization: `Bearer ${_authToken}` 8 | }, 9 | tags: Object.assign({}, { 10 | // all urls will be tagged with this name 11 | name: 'PrivateCrocs' 12 | }, 13 | // and any other name we pass through 14 | tag) 15 | }); 16 | } -------------------------------------------------------------------------------- /src/lib/sleep.helpers.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from "k6"; 2 | 3 | export function setSleep(min=1, max=2){ 4 | 5 | sleep(Math.floor(Math.random() * (max - min) + min)) 6 | 7 | } -------------------------------------------------------------------------------- /src/lib/test-data.helpers.ts: -------------------------------------------------------------------------------- 1 | export function randomString(length: number) : string { 2 | const charset = 'abcdefghijklmnopqrstuvwxyz'; 3 | let res = ''; 4 | while (length--) res += charset[Math.random() * charset.length | 0]; 5 | return res; 6 | } 7 | 8 | export function randomCrocodile(){ 9 | return { 10 | name: `Name ${randomString(10)}`, 11 | sex: 'M', 12 | date_of_birth: '2001-01-01', 13 | }; 14 | } -------------------------------------------------------------------------------- /src/lib/types/crocodile.api.d.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponseBody { 2 | "refresh": string; 3 | "access": string; 4 | } 5 | 6 | export interface Crocodile{ 7 | "id"?: number, 8 | "name": string, 9 | "sex": string, 10 | "date_of_birth": string, 11 | "age"?: number 12 | } -------------------------------------------------------------------------------- /src/lib/types/framework.types.d.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | first_name: string; 3 | last_name: string; 4 | username:string; 5 | password:string; 6 | } 7 | -------------------------------------------------------------------------------- /src/tests/create-crocs.seed.ts: -------------------------------------------------------------------------------- 1 | import { group } from 'k6'; 2 | import { Options } from 'k6/options'; 3 | 4 | import { randomString } from '../lib/test-data.helpers' 5 | import { createRequestConfigWithTag } from '../lib/request.helpers'; 6 | import { setSleep } from '../lib/sleep.helpers' 7 | 8 | import { User } from '../lib/types/framework.types' 9 | 10 | import * as crocodileOwnerActions from '../actions/roles/crocodile-owner.role' 11 | import * as adminActions from '../actions/roles/admin.role' 12 | import * as publicUserActions from '../actions/roles/public-user.role' 13 | import { Counter } from 'k6/metrics'; 14 | 15 | /** 16 | * This is a SEEDING script. Do not run as a performance test. 17 | * 18 | * It creates crocodiles on the following app: 19 | * https://test-api.loadimpact.com/ 20 | * for use by the performance tests (a Soak test in this case). 21 | */ 22 | 23 | // Test Options https://docs.k6.io/docs/options 24 | export let options: Partial = { 25 | // This script runs for 5 iterations and therefore creates 5 crocodiles. 26 | iterations: 5 27 | }; 28 | 29 | let numberOfCrocodilesCreated = new Counter("NumberOfCrocodilesCreated") 30 | 31 | const CROCODILE_OWNER: User = { 32 | first_name: "Crocodile", 33 | last_name: "Owner", 34 | username: `${randomString(10)}@example.com`, // Set your own email or `${randomString(10)}@example.com`;, 35 | password: 'superCroc2019' 36 | } 37 | 38 | const BASE_URL = 'https://test-api.loadimpact.com'; 39 | 40 | // The Setup Function is run once before the Load Test https://docs.k6.io/docs/test-life-cycle 41 | export function setup() { 42 | 43 | // admin user can create a new user without logging in - this is just a test system 44 | adminActions.registerNewUser(CROCODILE_OWNER, BASE_URL); 45 | 46 | // new user 'Crocodile Owner' logs in and returns the auth token 47 | const authToken = crocodileOwnerActions.login(CROCODILE_OWNER, BASE_URL); 48 | 49 | // anything returned here can be imported into the default function https://docs.k6.io/docs/test-life-cycle 50 | return authToken; 51 | } 52 | 53 | // default function (imports the Bearer token) https://docs.k6.io/docs/test-life-cycle 54 | export default (_authToken: string) => { 55 | 56 | // Private actions - you need to be logged in to do these 57 | group('Create crocs', () => { 58 | 59 | const requestConfigWithTag = createRequestConfigWithTag(_authToken); // Sets the auth token in the header of requests and the 'public requests' tag 60 | 61 | let URL = `${BASE_URL}/my/crocodiles/`; 62 | 63 | // returns an updated URL that contains the crocodile ID - could be saved to an array and eventually to a JSON file for use in performance tests. 64 | crocodileOwnerActions.createCrocodile(requestConfigWithTag, URL, numberOfCrocodilesCreated); 65 | 66 | }); 67 | 68 | // sleeps help keep your script realistic https://docs.k6.io/docs/sleep-t-1 69 | setSleep(); 70 | } 71 | 72 | export function teardown() { 73 | // you can add functionality here to save the crocodile IDs to a JSON file for use by the performance tests. 74 | } -------------------------------------------------------------------------------- /src/tests/soak.test.ts: -------------------------------------------------------------------------------- 1 | import { group } from 'k6'; 2 | import { Options } from 'k6/options'; 3 | 4 | import { randomString } from '../lib/test-data.helpers' 5 | import { createRequestConfigWithTag } from '../lib/request.helpers'; 6 | import { setSleep } from '../lib/sleep.helpers' 7 | 8 | import { Crocodile } from '../lib/types/crocodile.api' 9 | import { User } from '../lib/types/framework.types' 10 | 11 | import * as crocodileOwnerActions from '../actions/roles/crocodile-owner.role' 12 | import * as adminActions from '../actions/roles/admin.role' 13 | import * as publicUserActions from '../actions/roles/public-user.role' 14 | import { Counter } from 'k6/metrics'; 15 | 16 | /** 17 | * A soak test that runs through some common user actions 18 | * for the Crocodile App: 19 | * https://test-api.loadimpact.com/ 20 | * 21 | * P.s. the k6 Types can be found here for reference: 22 | * https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/k6 23 | */ 24 | 25 | // Test Options https://docs.k6.io/docs/options 26 | export let options: Partial = { 27 | // a single stage where we ramp up to 10 users over 30 seconds 28 | stages: [ 29 | { target: 20, duration: '3m' }, 30 | ], 31 | // test thresholds https://docs.k6.io/docs/thresholds 32 | thresholds: { 33 | 'http_req_duration': ['avg<500', 'p(95)<1500'], 34 | 'http_req_duration{name:PublicCrocs}': ['avg<400'], 35 | 'http_req_duration{name:Create}': ['avg<600', 'p(90)<700'], 36 | }, 37 | }; 38 | 39 | let numberOfCrocodilesCreated = new Counter("NumberOfCrocodilesCreated"); 40 | let numberOfCrocodilesDeleted = new Counter("NumberOfCrocodilesDeleted"); 41 | let numberOfCrocodilesUpdated = new Counter("NumberOfCrocodilesUpdated"); 42 | 43 | // Have these as gauges 44 | // Create a map that has 'vu' and 'count' 45 | // Then update the gauge with the count and then tag it with the vu both from the map 46 | 47 | let countCrocs = new Map(); 48 | let countDeleted = new Map(); 49 | let countUpdated = new Map(); 50 | 51 | /** 52 | * Example of importing data from a file - PLEASE NOTE we don't use this data, it's just to show how to do it 53 | * i.e. don't use the k6 'open()' function. 54 | * Webpack will automatically convert JSON to a JS object (don't need JSON.parse) 55 | * */ 56 | const crocodilesFromJson = require('../data/crocodiles.json') 57 | 58 | const CROCODILE_OWNER: User = { 59 | first_name: "Crocodile", 60 | last_name: "Owner", 61 | username: `${randomString(10)}@example.com`, // Set your own email or `${randomString(10)}@example.com`;, 62 | password: 'superCroc2019' 63 | } 64 | 65 | const BASE_URL = 'https://test-api.loadimpact.com'; 66 | 67 | // The Setup Function is run once before the Load Test https://docs.k6.io/docs/test-life-cycle 68 | export function setup() { 69 | 70 | // admin user can create a new user without logging in - this is just a test system 71 | adminActions.registerNewUser(CROCODILE_OWNER, BASE_URL); 72 | 73 | // new user 'Crocodile Owner' logs in and returns the auth token 74 | const authToken = crocodileOwnerActions.login(CROCODILE_OWNER, BASE_URL); 75 | 76 | // anything returned here can be imported into the default function https://docs.k6.io/docs/test-life-cycle 77 | return authToken; 78 | } 79 | 80 | // default function (imports the Bearer token) https://docs.k6.io/docs/test-life-cycle 81 | export default (_authToken:string) => { 82 | 83 | // Public actions - you don't need to be logged in to perform these 84 | // this is a group https://docs.k6.io/docs/tags-and-groups 85 | group('Query and Check Crocs', () => { 86 | 87 | let crocodiles: Crocodile[] = []; 88 | crocodiles = publicUserActions.queryCrocodiles(BASE_URL, crocodiles); 89 | publicUserActions.checkAges(crocodiles, 5) 90 | 91 | }) 92 | 93 | // Private actions - you need to be logged in to do these 94 | group('Create and Modify Crocs', () => { 95 | 96 | const requestConfigWithTag = createRequestConfigWithTag(_authToken); // Sets the auth token in the header of requests and the 'public requests' tag 97 | let URL = `${BASE_URL}/my/crocodiles/`; 98 | 99 | // returns an updated URL that contains the crocodile ID 100 | URL = crocodileOwnerActions.createCrocodile(requestConfigWithTag, URL, numberOfCrocodilesCreated); 101 | 102 | crocodileOwnerActions.updateCrocodile(requestConfigWithTag, URL, "New Name", numberOfCrocodilesUpdated); 103 | 104 | crocodileOwnerActions.deleteCrocodile(requestConfigWithTag, URL, numberOfCrocodilesDeleted); 105 | 106 | }); 107 | 108 | // sleeps help keep your script realistic https://docs.k6.io/docs/sleep-t-1 109 | setSleep(); 110 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "compilerOptions": { 4 | 5 | // Target ECMAScript. 6 | 7 | "target": "ES6", 8 | 9 | // Search under node_modules for non-relative imports. 10 | 11 | "moduleResolution": "node", 12 | 13 | // Process & infer types from .js files. 14 | 15 | "allowJs": true, 16 | 17 | // Don't emit; allow Babel to transform files. 18 | 19 | "noEmit": true, 20 | 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | 23 | "strict": true, 24 | 25 | // Disallow features that require cross-file information for emit. 26 | 27 | "isolatedModules": true, 28 | 29 | // Import non-ES modules as default imports. 30 | 31 | "esModuleInterop": true, 32 | 33 | "skipLibCheck": true, 34 | 35 | "noImplicitThis": true, 36 | 37 | "strictNullChecks": true, 38 | 39 | }, 40 | 41 | "include": [ 42 | 43 | "src" 44 | 45 | ] 46 | 47 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 5 | resolve: { 6 | 7 | extensions: ['.ts', '.js'] 8 | 9 | }, 10 | 11 | mode: 'development', 12 | 13 | entry: { 14 | 15 | soakTest: './src/tests/soak.test.ts', 16 | seedCrocs: './src/tests/create-crocs.seed.ts', 17 | 18 | }, 19 | 20 | output: { 21 | 22 | path: path.resolve(__dirname, 'dist'), 23 | 24 | libraryTarget: 'commonjs', 25 | 26 | filename: '[name].bundle.js' 27 | 28 | }, 29 | 30 | module: { 31 | 32 | rules: [ 33 | 34 | { 35 | 36 | test: /\.ts$/, 37 | 38 | // exclude: /node_modules/, 39 | 40 | loader: 'babel-loader', 41 | 42 | options: { 43 | 44 | presets: [['@babel/typescript']], 45 | 46 | plugins: [ 47 | 48 | '@babel/proposal-class-properties', 49 | 50 | '@babel/proposal-object-rest-spread' 51 | 52 | ] 53 | 54 | } 55 | 56 | } 57 | 58 | ] 59 | 60 | }, 61 | 62 | stats: { 63 | 64 | colors: true 65 | 66 | }, 67 | 68 | // target: 'web', 69 | 70 | externals: /k6(\/.*)?/, 71 | 72 | devtool: 'source-map' 73 | 74 | }; --------------------------------------------------------------------------------