Skip to content

Commit 1d5a6c8

Browse files
authored
Merge pull request #34 from FreakeyPlays/k6
test(stress): added Stress-Testing using k6
2 parents 36c4775 + 9322784 commit 1d5a6c8

24 files changed

+497
-2
lines changed

.github/workflows/ci.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,30 @@ jobs:
2525

2626
- name: Stop Container
2727
run: docker-compose down
28+
29+
k6:
30+
name: 🧪 Stress - Testing
31+
runs-on: ubuntu-latest
32+
defaults:
33+
run:
34+
working-directory: ./k6
35+
36+
steps:
37+
- name: Checkout Code
38+
uses: actions/checkout@v3
39+
40+
- name: Run k6 Tests
41+
uses: grafana/k6-action@v0.3.0
42+
with:
43+
filename: k6/main.js
44+
flags: --summary-export=summary.json
45+
env:
46+
STAGE: prod
47+
TYPE: smoke
48+
49+
- name: Upload Summary
50+
uses: actions/upload-artifact@v3.1.2
51+
with:
52+
name: k6-Summary
53+
path: summary.*
54+
retention-days: 2

.gitignore

+9-1
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,12 @@ client/src/environments
7878
npm-debug.log
7979
yarn-error.log
8080
testem.log
81-
/typings
81+
/typings
82+
83+
# test summary
84+
k6/summary.json
85+
k6/summary.html
86+
87+
# test output
88+
cypress/videos
89+
cypress/screenshots

Makefile

+10-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,13 @@ stop:
1111
docker-compose -f docker-compose.yml stop
1212

1313
remove:
14-
docker-compose -f docker-compose.yml down
14+
docker-compose -f docker-compose.yml down
15+
16+
cypress-cli:
17+
npx cypress run
18+
19+
cypress-ui:
20+
npx cypress open
21+
22+
test-k6:
23+
docker run --net=host --rm -v ${pwd}/k6:/e2e -e STAGE=dev --workdir /e2e -i loadimpact/k6 run /e2e/main.js --summary-export=/e2e/summary.json

k6/constants.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export const SLEEP = {
2+
MIN: 1,
3+
MAX: 5
4+
}
5+
6+
/**
7+
* For this Project the virtual uses and duration are set to small values.
8+
* To see Real world examples see: https://k6.io/docs/test-types/load-test-types/
9+
*/
10+
export const TEST_TYPES = {
11+
smoke: {
12+
// Test the minimal load
13+
vus: 3, // Little virtual Users
14+
duration: '15s' // Only a few iterations
15+
},
16+
load: {
17+
// Test the average load
18+
stages: [
19+
{ duration: '5s', target: 5 }, // Ramp-up from 1 to 10 virtual users
20+
{ duration: '30s', target: 5 }, // Stay at 10 virtual users for 90s
21+
{ duration: '5s', target: 0 } // Ramp-down to 0 virtual users
22+
]
23+
},
24+
stress: {
25+
// Test heavy load
26+
stages: [
27+
{ duration: '30s', target: 20 }, // Ramp-up from 1 to 20 virtual users
28+
{ duration: '90s', target: 20 }, // Stay at 20 virtual users for 90 seconds
29+
{ duration: '15s', target: 0 } // Ramp-down to 0 virtual users
30+
]
31+
},
32+
soak: {
33+
// Test average load over a longer time
34+
stages: [
35+
{ duration: '30s', target: 10 }, // Ramp-up to 10 virtual users
36+
{ duration: '1m', target: 10 }, // Stay at 10 virtual users for 1 minute
37+
{ duration: '30s', target: 0 }, // Ramp-down to 0 virtual users
38+
{ duration: '1m', target: 0 } // Do nothing for 1 minute
39+
]
40+
},
41+
spike: {
42+
// Test sudden load
43+
stages: [
44+
{ duration: '30s', target: 100 }, // Ramp-up to 100 virtual users
45+
{ duration: '15s', target: 0 } // Rapidly decrease virtual users to 0
46+
]
47+
},
48+
break: {
49+
// Find the Limits of the tested system
50+
executor: 'ramping-arrival-rate', // Assure load increase if the system slows
51+
stages: [
52+
{ duration: '5m', target: 250 } // Slowly Ramp-up to a Huge load
53+
]
54+
}
55+
}

k6/data/endpoint.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { config } from '../env.js'
2+
3+
export const TODO_ENDPOINT = {
4+
todo: {
5+
createToDo: `${config().host.toDoApi}/todo`,
6+
returnTodoById: id => {
7+
return `${config().host.toDoApi}/todo/${id}`
8+
},
9+
returnAllToDo: `${config().host.toDoApi}/todo`,
10+
editToDo: id => {
11+
return `${config().host.toDoApi}/todo/${id}`
12+
},
13+
deleteToDo: id => {
14+
return `${config().host.toDoApi}/todo/${id}`
15+
},
16+
deleteAllToDo: `${config().host.toDoApi}/todo/remove/all`
17+
}
18+
}

k6/data/todo/create-todo.data.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { randomBool, randomInt } from '../../utils.js'
2+
3+
export const CREATE_TODO_DATA = {
4+
label: 'Learn coding every Monday',
5+
priority: () => randomInt(0, 10),
6+
done: randomBool,
7+
position: 0
8+
}

k6/data/todo/update-todo.data.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { randomBool, randomInt } from '../../utils.js'
2+
3+
export const UPDATE_TODO_DATA = {
4+
label: 'Learn coding every Sunday',
5+
priority: () => randomInt(0, 10),
6+
done: randomBool,
7+
position: 0
8+
}

k6/env.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export const env = {
2+
dev: {
3+
host: {
4+
toDoApi: 'http://localhost:8081'
5+
}
6+
},
7+
prod: {
8+
host: {
9+
toDoApi: 'https://todo-api.chrismerck.me'
10+
}
11+
}
12+
}
13+
14+
const DEV = 'dev'
15+
const PROD = 'prod'
16+
17+
export const config = () => {
18+
switch (__ENV.STAGE) {
19+
case DEV:
20+
return env.dev
21+
case PROD:
22+
return env.prod
23+
default:
24+
throw `[k6] - Stage ${__ENV.STAGE} not found`
25+
}
26+
}

k6/helper/todo.helper.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { CREATE_TODO_DATA } from '../data/todo/create-todo.data.js'
2+
import * as Step from '../step/index.js'
3+
4+
export default function getTodoId() {
5+
const response = Step.createToDo(
6+
CREATE_TODO_DATA.label,
7+
CREATE_TODO_DATA.priority(),
8+
CREATE_TODO_DATA.done(),
9+
CREATE_TODO_DATA.position
10+
)
11+
12+
return response.json().id
13+
}

k6/main.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js'
2+
import { htmlReport } from 'https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js'
3+
import { TEST_TYPES } from './constants.js'
4+
import getTodoId from './helper/todo.helper.js'
5+
import * as Step from './step/index.js'
6+
import * as Test from './tests/index.js'
7+
8+
const SELECTED_TEST = TEST_TYPES[__ENV.TYPE ? __ENV.TYPE : 'smoke']
9+
const BASE_OPTIONS = {
10+
tags: { type: 'API' },
11+
iterations: 3,
12+
noConnectionReuse: true,
13+
thresholds: {
14+
checks: ['rate<90'],
15+
http_req_duration: ['p(95)<250'],
16+
http_req_failed: ['rate<5']
17+
}
18+
}
19+
20+
export let options = Object.assign({}, BASE_OPTIONS, SELECTED_TEST)
21+
22+
export function setup() {
23+
return getTodoId()
24+
}
25+
26+
export default function (id) {
27+
Test.createToDo()
28+
Test.updateToDo(id)
29+
Test.readTodoById(id)
30+
Test.readAllToDo()
31+
Test.deleteToDo(id)
32+
}
33+
34+
export function teardown() {
35+
Step.deleteAllToDo()
36+
}
37+
38+
export function handleSummary(data) {
39+
return {
40+
'summary.html': htmlReport(data, {
41+
title: 'ToDo Test Summary',
42+
template: 'bootstrap'
43+
}),
44+
stdout: textSummary(data, { indent: ' ', enableColors: true })
45+
}
46+
}

k6/step/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { default as createToDo } from './todo/create-todo.step.js'
2+
export { default as deleteAllToDo } from './todo/delete-all-todo.step.js'
3+
export { default as deleteToDo } from './todo/delete-todo.step.js'
4+
export { default as readAllToDo } from './todo/read-all-todo.step.js'
5+
export { default as readToDoById } from './todo/read-todo-by-id.step.js'
6+
export { default as updateToDo } from './todo/update-todo.step.js'

k6/step/todo/create-todo.step.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function (label, priority, done, position) {
5+
const url = TODO_ENDPOINT.todo.createToDo
6+
7+
const payload = JSON.stringify({
8+
label: label,
9+
priority: priority,
10+
done: done,
11+
position: position
12+
})
13+
14+
const params = {
15+
headers: {
16+
'Content-Type': 'application/json'
17+
}
18+
}
19+
20+
return http.post(url, payload, params)
21+
}

k6/step/todo/delete-all-todo.step.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function () {
5+
const url = TODO_ENDPOINT.todo.deleteAllToDo
6+
7+
const params = {
8+
headers: {
9+
'Content-Type': 'application/json'
10+
}
11+
}
12+
13+
return http.del(url, undefined, params)
14+
}

k6/step/todo/delete-todo.step.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function (id) {
5+
const url = TODO_ENDPOINT.todo.deleteToDo(id)
6+
7+
const params = {
8+
headers: {
9+
'Content-Type': 'application/json'
10+
}
11+
}
12+
13+
return http.del(url, undefined, params)
14+
}

k6/step/todo/read-all-todo.step.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function () {
5+
const url = TODO_ENDPOINT.todo.createToDo
6+
7+
const params = {
8+
headers: {
9+
'Content-Type': 'application/json'
10+
}
11+
}
12+
13+
return http.get(url, undefined, params)
14+
}

k6/step/todo/read-todo-by-id.step.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function (id) {
5+
const url = TODO_ENDPOINT.todo.returnTodoById(id)
6+
7+
const params = {
8+
headers: {
9+
'Content-Type': 'application-json'
10+
}
11+
}
12+
13+
return http.get(url, undefined, params)
14+
}

k6/step/todo/update-todo.step.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import http from 'k6/http'
2+
import { TODO_ENDPOINT } from '../../data/endpoint.js'
3+
4+
export default function (id, label, priority, done, position) {
5+
const url = TODO_ENDPOINT.todo.editToDo(id)
6+
7+
const payload = JSON.stringify({
8+
label: label,
9+
priority: priority,
10+
done: done,
11+
position: position
12+
})
13+
14+
const params = {
15+
headers: {
16+
'Content-Type': 'application/json'
17+
}
18+
}
19+
20+
return http.put(url, payload, params)
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { initContractPlugin } from 'https://jslib.k6.io/k6chaijs-contracts/4.3.4.0/index.js'
2+
import {
3+
chai,
4+
describe,
5+
expect
6+
} from 'https://jslib.k6.io/k6chaijs/4.3.4.1/index.js'
7+
import { sleep } from 'k6'
8+
import { CREATE_TODO_DATA } from '../../../data/todo/create-todo.data.js'
9+
import * as Step from '../../../step/index.js'
10+
import { randomSleep } from '../../../utils.js'
11+
12+
initContractPlugin(chai)
13+
14+
export default function () {
15+
describe('Create ToDo : Success', () => {
16+
const response = Step.createToDo(
17+
CREATE_TODO_DATA.label,
18+
CREATE_TODO_DATA.priority(),
19+
CREATE_TODO_DATA.done(),
20+
CREATE_TODO_DATA.position
21+
)
22+
23+
if (response.status != 201) {
24+
console.log(response.body)
25+
}
26+
27+
expect(response.status, 'API status code').to.equal(201)
28+
expect(response).to.have.validJsonBody()
29+
30+
sleep(randomSleep())
31+
})
32+
}

0 commit comments

Comments
 (0)