For this workshop you need:
- k6
git
and a shell- Your preferred editor/IDE for JavaScript, e.g., VS Code
- Clone the repository:
git clone https://github.com/bekk/k6-workshop
. - You should have received a provisioned app by the workshop facilitator. The URL should be on the format
https://api.<xx>.cloudlabs-azure.no
. - The app is available at
https://api.<xx>.cloudlabs-azure.no
. Verify the app is running correctly by runningcurl https://api.<xx>.cloudlabs-azure.no/healthcheck
, or openinghttps://api.<xx>.cloudlabs-azure.no/healthcheck
in the browser. You should see a message stating that the database connection is ok.
You should now be ready to go!
-
Open
load-tests/contants.js
, and updateBASE_URL
with yourhttps://api.<xx>.cloudlabs-azure.no
URL. -
In the
load-tests
folder, create a file namedcreate-users.js
, and put the following code in it:
import http from 'k6/http'
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'
import { BASE_URL } from './constants.js'
export default function() {
const username = `user-1a-${randomIntBetween(0, 1000000000)}`;
const email = `${username}@test.no`
http.post(`${BASE_URL}/users`, JSON.stringify({
username: username,
email: email
}), {
headers: { 'Content-Type': 'application/json' },
})
}
-
From the command line in the
load-tests
folder, run:k6 run create-users.js
. This should produce some output, including a table of statistics. Right now we want the row starting withhttp_req_failed
. This should say0.00%
, meaning all (in this case, a single request) succeeded with a successful status code.So, what happens when k6 runs the file?
- The default exported function in the file is assumed to be the load test by k6.
http.post
performs a single HTTP POST request towards the/users
endpoint, with the payload and headers specified in the code. The payload object is converted to string by usingJSON.stringify
, since k6 doesn't do that for us.http
is a built-in k6 library that is needed for generating proper statistics for requests and responses, usingfetch
or other native JS methods won't work properly.randomIntBetween
is fetched from an external k6 library,k6-utils
, that is maintained by Grafana Labs.
-
Let's generate more than one request! 📈
Run:
k6 run --duration 5s --vus 100 create-users.js
Here, we simulate 100 virtual users (VUs) making continuous requests for 5 seconds. Depending on hardware, OS and other factors, you might see warnings about dropped connections. Take a look at the
iterations
field in the output, which shows the total number of times the default exported function is run (the above command should run at least 200 iterations towards a B1 tier Azure App Service).You can follow the progress in your browser by using the k6 web dashboard. Set
K6_WEB_DASHBOARD=true
for k6 to start it, andK6_WEB_DASHBOARD_OPEN=true
to automatically open it in your browser (see below for shell-specific instructions). You should have a duration of at least 30 seconds to get interesting visualizations in the dashboard.k6 web dashobard in Bash/Zsh
In your shell, run the following commands:
export K6_WEB_DASHBOARD=true export K6_WEB_DASHBOARD_OPEN=true
k6 web dashobard in PowerShell
In your shell, run the following commands:
$Env:K6_WEB_DASHBOARD='true' $Env:K6_WEB_DASHBOARD_OPEN='true'
Try experimenting with different VUs and durations to see what the limits of the system and network you're testing are. Please note:
-
Your network resources are limited by your operating system configuration and network, you will likely see warnings emitted from the k6 runtime similar to some of the below:
WARN[0047] Request Failed error="Post \"https://api.00.cloudlabs-azure.no/users\": dial: i/o timeout" WARN[0023] Request Failed error="Post \"https://api.00.cloudlabs-azure.no/users\": dial tcp 20.105.216.18:443: connect: can't assign requested address"
For the workshop, this means you've reached the limits and there's no point in scaling further, for now. In a real-world use case, you'd probably want to look at options to scale k6 or tuning the OS.
-
The test will run longer than the allotted duration. This is because each running VU is given 30 seconds to exit before it's killed (this timeout is configurable).
-
⚠️ Doing this in the cloud might incur unexpected costs and/or troubles for other services on shared infrastructure! In this workshop, everyone runs separate instances that does not autoscale, so no additional costs will incur.
-
-
The previous test is not realistic, because it's the equivalent of 100 users simultaneously clicking a "Create user" button as fast as they can. Therefore,
sleep
can be used to simulate natural delays in usage, e.g., to simulate polling at given intervals, delays caused by network, user interactions and/or rendering of UI. At the top of the file, addimport { sleep } from 'k6'
, and at the bottom of the function (after thehttp.post
) addsleep(1)
to sleep for one second. Running the command from the previous step would result in about 500 iterations (5s * 100 users) without network delay, but throttling (network, database, etc.) will cause additional delays and fewer iterations.💡 If you can look at the logs, observe that the requests occur in batches. All virtual users run their requests at approximately the same time, then sleeps for a second, and so on. We'll look at how to more realistically distribute the load using other methods later.
We also want to verify that the response is correct. We can use check
, to test arbitrary conditions and get the result in the summary report.
const response = http.post(...)
// A check can contain multiple checks, and returns true if all checks succeed
check(response, {
'200 OK': r => r.status == 200 ,
'Another check': r => ...
})
-
Get the response from the POST request, and verify that the response status code is
200
usingcheck
. Remember to addcheck
to the import fromk6
(import { check, sleep } from 'k6'
). Run k6 like before, and see that the checks pass as expected.You can get the response body by using the
Response.json()
method:const response = http.post('[...]/users', ...) // This call is unchanged from the previous step const createdUser = response.json()
When testing new code, running with 1 VU for 1 iteration is enough to test the code:
k6 --iterations 1 --vus 1 create-users.js
. You can usek6 --vus 100 --duration 5s create-users.js
when you want to test performance. -
Add a call to
check
that verifies thatusername
andemail
in the response corresponds with what was sent in the request. Test your solution by running k6 like before.Hint: Calling
check(...)
Your code should now look something like this:
export default function() { const username = `user-1a-${randomIntBetween(0, 1000000000)}`; const email = `${username}@test.no` const response = http.post(`${BASE_URL}/users`, JSON.stringify({ username: username, email: email }), { headers: { 'Content-Type': 'application/json' }, }) const createdUser = response.json() check(createdUser, { 'username is correct': (u) => u.username == username, 'email is correct': (u) => u.email == email }) }
💰 Extra credit: What if the request fails?
The request can fail, especially when load testing, resulting in errors from the k6 runtime when trying to call
Response.json()
. An easy way to handle that is to wrap the code using the response body in anif
block.See what happens if you try performing a request to an invalid URL, fix it and then test again. (Make sure to correct the URL again, before moving on to the next task.)
ℹ️ You can check out the documentation for more information about check
.
You might already have encountered non-successful error codes. Trying to create a user with a non-unique username or email will return a 409
response. This is a valid response, so we'd like this to count as a successful HTTP response in our statistics. (This might not be what you normally want, depending on what you actually want to test.)
-
Make the range of random numbers generated for the username smaller by changing the first line in the function to
const username = `user-1d-${randomIntBetween(0, 100)}`;
. Increase the duration and VUs usingk6 run --duration 15s --vus 200 create-users.js
, and see that your tests and checks fail when running k6. -
We can specify that
409
is an expected status code for thehttp.post
call. We will use a responseCallback and give it as a parameter to thehttp.post
method. Modify thehttp.post
method to add theresponseCallback
const response = http.post(`...`, JSON.stringify({
// ... - like before
}), {
headers: { 'Content-Type': 'application/json' },
responseCallback: http.expectedStatuses(200, 409)
})
k6 will now consider 409
a valid response status code. Also remove or modify the previous check for 200 response status code. Run k6 again, verify that all checks succeed and http_req_failed
is 0.00%
.
💡 Remember to adjust back to randomIntBetween(0, 100000000)
before moving on.
ℹ️ You can read more about configuring expectedStatuses
, responseCallback
and http.*
method params in the documentation.
http.get(url)
can be used to perform a GET request to the supplied url
and generate statistics for the summary report. The URL to get the user with id
is /users/[id]
. You can get the id
from the response after creating the user:
const createdUser = response.json();
// ... - check omitted
const id = createdUser.id;
-
Add a request to get the user using
http.get(...)
. Test that it works as expected.Hint: Calling
http.get(...)
http.get()
does not need any extra arguments than the URL:const getUserResponse = http.get(`${BASE_URL}/users/${id}`)
-
Add a check for the response, verifying that the status and
id
in the returned body is correct. This should be done with a separate call tocheck
, after thegetUserResponse
. Remember to test your code.
Some services might have service level objectives (SLOs), either defined externally or as an internal metric by the team. These might specified like "99% of requests should be served in less than 500ms". Thresholds can be used to represent SLOs, and are specified in a specially exported options object:
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // http errors should be less than 1%
http_req_duration: ['avg<200', 'p(99)<500'], // average request duration should be below 200ms, 99% of requests below 500ms
},
};
export default function () {
// ...
}
These thresholds use the built-in metrics to decide whether the system under test has acceptable performance.
-
Create thresholds for testing that 95% requests are served in less than 500ms and the average response duration is less than 300ms (using the
http_req_duration
Trend metric). Run the test with 100 VUs over 10 seconds, and see if they succeed.💡 k6 displays the error in multiple ways by default. The default summary contains a red cross or green checkmark next to the metric, and an error message is also printed. In addition, the exit code is also erroneous, which is very useful for automated performance testing using CI/CD.
-
Modify the threshold so that it succeeds.
ℹ️ The documentation gives a good overview of different ways to use thresholds.
-
Add a new threshold:
'http_reqs{status:500}': ['count<1']
. This threshold is using the built-inhttp_reqs
Counter metric, and filtering on thestatus
tag, and setting the threshold to less than 1 (i.e., none), so that the load test will fail if we get500
responses. Run the test with 1000 VUs over 10 seconds, and look at how it fails. Look for{ status:500 }
in the output, it should have a check mark or red x next to it. -
If you want to track a metric (i.e., get it in the output summary), you can add a threshold without checks. Add the threshold
'http_reqs{status:200}': []
and run the tests again. Notice that{ status:200 }
does not have a check mark or red x, because it doesn't have any thresholds associated with it.ℹ️ There are different types of metrics, and k6 has many built-in metrics and functionality for creating custom metrics. Groups and tags are useful to filtering results in scenarios and complex tests.
The code to call HTTP methods requires some boilerplate. All endpoints you need to use in this tutorial have a corresponding helper function in load-tests/utils.js
. The functions abstract away the Content-Type
header, a responseCallback
and a check
on response status code. The params can be overridden by passing the params
argument to the helper functions. The helper functions return null
if they fail, otherwise the response returned when calling response.json()
. Use the helper functions unless otherwise specified in all following tasks. Take a look in load-tests/utils.js
for more information about the helper functions.
- Modify your code to use the
createUser(username, email, params)
andgetUser(id, params)
instead ofhttp.post
andhttp.get
respectively. Test that it works as expected.
We'll now move on to creating todo lists. In our domain, a single user can create many todo lists. There's no need to add sleep
calls, we'll get to that in later sub-tasks.
-
Create a new file,
create-todo-lists.js
. Create a user like in previous tasks, using thegetUser
function. -
If the user is created correctly, create a todo list. A todo lists can be created with
createTodoList(ownerId, name[, params])
. TheownerId
must correspond to an existing user, and thename
can be any string. The endpoint returns a todo list object:{ id: int, ownerId: int, name: string }
. -
Delete the user with
deleteUser(userId[, params])
after creating the todo list to clean up. The application uses cascading deletes, so there's no need to delete the todo list first, it will be deleted automatically. -
Verify that your code runs correctly.
ℹ️ Normally, running tests locally or in a pre-production environment, you wouldn't need to delete users. If you can assign IDs based on e.g. time or UUIDs, you probably wouldn't need to worry. However, we're doing it in this tutorial to avoid the ID-generating boilerplate and to demonstrate ways to do it.
In task 2a, we had a lot of code to create and delete users just for testing the performance of creating a todo list. k6, like other testing frameworks, support setup and teardown functions. We'll refactor the code to use those.
Here's a rough overview of how the test life cycle works:
// 1. init code - runs once per virtual user
export function setup() {
// 2. setup code - runs once
// returns data
}
export default function (data) {
// 3. VU code - repeated as many times as necessary
// Can use data from setup function
}
export function teardown(data) {
// 4. teardown code - runs once, if the setup function completes successfully
// Can use data from setup function
}
ℹ️ The test lifecycle documentation page has more information about the k6 test life cycle.
-
Create
setup
andteardown
functions that creates and deletes a user, respectively.The
setup
function should return theid
of the generated user, and both the VU function (theexport default function
you've already created) and theteardown
function should take theid
as parameter, avoiding global variables. This way we only create a single user for the test, instead of one user per iteration. As an added benefit, the code should be simpler to read. -
Run with 10 VUs for 5 seconds. Notice that checks in
setup
andteardown
are grouped separately in the output.
We've already used options
for defining thresholds. Options can be used for many things, including overriding user agents, DNS resolution or specifying test run behavior. We'll now look at two simple ways to define duration and VUs in the script.
One way is to define a constant number of VUs for a given amount of time. This is equivalent to specifying --vus
and --duration
when invoking k6 run
:
export const options = {
duration: '30s',
vus: 2,
}
Another way is to specify stages
, which changes the number of the VUs dynamically. This will use a "ramping VUs executor" by default, but we'll look more into different executors later.
export const options = {
stages: [
{ duration: '10s', target: 1 },
{ duration: '15s', target: 16 },
{ duration: '45s', target: 200 },
],
}
ℹ️ You can read more about options in the documentation.
-
Create the
options
object, and specify 10 VUs for a duration of 5 seconds. Runk6 run create-todo-lists.js
(without the--vus
and--duration
CLI arguments) and verify from the output that it works. -
Ramp up and down the number of VUs dynamically using the
stages
property in theoptions
object. Removevus
andduration
fromoptions
. Define stages to ramp up to 10 VUs in 10 sec, then up to 100 VUs in 20 sec, and finally down to 5 VUs in 10 sec. When running it, notice how the number of VUs increase and decrease in the output, like in the example below:
running (0m23.4s), 069/100 VUs, 4181 complete and 0 interrupted iterations
default [=====================>----------------] 069/100 VUs 23.3s/40.0s
- Options can be overridden by CLI arguments. Without modifying the code, use CLI arguments to run with 10 VUs for 2 seconds. (Use
--duration
and--vus
arguments like before.)
ℹ️ You can read more about the order of precedence of CLI arguments in the documentation.
So far, we've used the setup
and teardown
stages, which runs once per test invocation. We'll now look at different ways to create test data using the init
stage. This is just a fancy name for putting code outside a function, i.e. in the global scope. The code in the init
scope will generally be run once per VU, and can be used to generate useful test code. The init
stage restricts HTTP calls, meaning that it's not possible to call any of the http.*
functions to create test data. setup()
must be used instead if you need to perform HTTP requests.
-
In the
init
stage, create a list of ten random todo list names,todoListNames
, using afor
-loop and therandomIntBetween
function fromutils.js
that's also used for creating unique usernames. In the VU stage function, use a random name from the array every time (i.e., use this:todoListNames[randomIntBetween(0, todoListNames.length)]
). This illustrates how to create unique variables per VU, which can be useful for generating test data. Run the test and verify everything works correctly.Hint: creating the array
const todoListNames = [] for (let i = 0; i < 10; i++) { todoListNames.push(`Todo list name #${randomIntBetween(0, 100000)}`) }
-
The previous example generates unique test data per VU. It is also possible to share (read-only) test data between VUs using
SharedArray
.SharedArray
will create a shared array once for all VUs, saving a lot of memory when running many VUs in parallel. ASharedArray
can be created by modifying the code like this:const todoListNames = new SharedArray('todoListNames`, function() { /* code creating the array and returning it */ })
Modify the code from the previous step to use a
SharedArray
and verify that it still works correctly. You will needimport { SharedArray } from "k6/data"
at the top of the file.Hint: creating a
SharedArray
const todoListNames = new SharedArray('todoListNames', function() { const todoListNames = [] for (let i = 0; i < 10; i++) { todoListNames.push(`Todo list name #${randomIntBetween(0, 100000)}`) } return todoListNames; })
ℹ️ Take a look at the documentation for SharedArray
for more information about SharedArray
and its performance characteristics.
Like mentioned in task 2d, we can't call http.*
functions to create test data. So, if we want pre-created users for each VU, we'll need to create them in the setup()
function. How do we do that? Note that in this case, we'll assume the maximum number of VUs are 100.
-
Modify
setup()
to create 100 users (usingcreateUser()
), and return a list of user IDs. Modifyteardown()
to loop over the list it now receives as a parameter, and delete every user.Hint: loops the
setup()
andteardown()
functionsThe
setup()
function is similar to before, but with the code wrapped in a loop to create an array to be returned:export function setup() { const userIds = []; for (let i = 0; i < 100; i++) { const username = `user-2d-${randomIntBetween(0, 100000000)}`; const email = `${username}@test.no` const createdUser = createUser(username, email); userIds.push(createdUser.id); } return userIds; }
A loop is also introduced for
teardown()
:export function teardown(userIds) { for (let i = 0; i < 100; i++) { deleteUser(userIds[i]); } }
-
The main VU function must be modified to take a list of user IDs. To assign a unique ID per VU, we will use the execution context variable
vu.idInTest
(requiresimport { vu } from "k6/execution"
). This will be a number between 1 and 100 (inclusive), and can be used to index the array to retrieve a unique user id.Hint: using an execution context variable
Instead of getting
userId
as a parameter, we usevu.idInTest - 1
to index theuserIds
argument. \thecreateTodoList(...)
statement is unchanged.export default function(userIds) { const userId = userIds[vu.idInTest-1]; createTodoList(userId, todoListNames[randomIntBetween(0,todoListNames.length)]); }
-
Run the test, and verify that it runs correctly. If you look at the application logs, notice that the
ownerId
for created todo lists varies.ℹ️ There are many execution context variables to choose from.
We've now been through the simplest usage of k6. The following tasks will contain less guidance, and you will likely need to read the documentation when links are provided to learn and figure out how to do it.
You can do the tasks in almost any order, depending on what you're interested in.
In this task we'll work with scenarios. Scenarios are useful for simulating realistic user/traffic patterns and gives lots of flexibility in load tests. A scenario is a single traffic pattern, and by combining scenarios, we can simulate complex usage patterns. For instance, imagine the following scenarios:
- Scenario 1: About 20 users signs up each hour. Each user creates a todo list, and adds a couple of todos.
- Scenario 2: The application has a baseline of 20 users 24/7, creating, modifying and deleting todos on existing todo lists. On top of the baseline usage, the traffic starts increasing about 6 in the morning, ramps up to about 100 (total) users by 11 and stays like that until 15, when it starts to slowly decrease down to the baseline of 20 users again.
- Scenario 3: Your new fancy todo app gains popularity on Hacker News, and at any giving time during the lunch hour (11-12) around 100 users sign up.
- Scenario 4: A batch job runs every hour, deleting around 20 inactive users each time.
All of these scenarios have different traffic patterns, and we control them by using executors. Executors can control arrival rate, ramping up and down and iteration distribution between VUs. Read briefly through the main scenarios documentation page to get an overview of scenarios, executors and the syntax to create them.
We'll implement all of these scenarios, but for the sake of the tutorial and short load test runs, we'll assume 1 (real-life) hour is 5 seconds.
utils.js
has three helper methods for working with todos: createTodo(todoListId, description, completed, [params])
, patchTodo(todoListId, todoId, [description], [completed], [params])
and deleteTodo(todoListId, todoId, [params])
, all of which return the state of the modified todo.
-
Create a new file,
create-todo-scenarios.js
. -
Let's start with scenario 1. In the
export default function
, create a user, create a todo list for the user and add 5 todos to the list. The description of the todo does not matter. There's a 50ms delay between adding each todo (usesleep(0.05)
).We'll make this a scenario in the next step, for now verify that the code works by running k6 with a low number of VUs for a short duration.
💰 Extra credit: Making the traffic even more realistic
Create a randomized number of todos (3-10) in the todo list (use
randomIntBetween
) to better simulate realistic traffic. -
We'll assume the users signing up are evenly spread out inside the time range, and we'll therefore use the constant arrival rate executor. What should
duration
,rate
andtimeUnit
be for this scenario (remember, 1 hour "real time" = 5 seconds)? Create anoptions
object (like previous tasks, remember toexport
it!), and add asignups
scenario, using constant arrival rate, theduration
,rate
andtimeUnit
you decided, andpreAllocatedVUs
= 1.Hint:
duration
,rate
andtimeUnit
duration = 24h * 5s/1h = 120s
Since we have 20 new users (= VU function invocations) per "hour" (= 5s) we have:
duration = 120s
,rate = 20
andtimeUnit = 5s
.
Run k6 (without duration and VUs CLI arguments!). You should get a warning about "Insufficient VUs", but let the test continue to run. Notice the dropped_iterations
metrics after the test is completed. This should usually be zero, but misconfiguration of executors can cause errors. dropped_iterations
can also be caused by overloading the system under test.
ℹ️ Take a look at the docs to learn more about dropped_iterations
.
-
Set
preAllocatedVUs
to 5, andmaxVUs
to 50 for the scenario and run the test again. Observe that the number of VUs might scale during the test run, in order to serve requests fast enough. -
To accommodate for multiple scenarios, we will stop using
export default function()
and instead use "scenario functions". Removedefault
and name the functionsignups
. In the definition of the scenario (in theoptions
object), addexec: 'signups'
. Read more in the additional lifecycle functions documentation. Re-run the test to verify that it still works.
-
Moving on to scenario 2. Create 100 test users with corresponding todo lists in
setup()
, using a similar approach to what you did in task 2e. Create a function calledtodos
for our newtodos
scenario - it should select its todo list from the parameter, create, patch and delete some todos.sleep
in between operations (50-150ms should be enough).To simulate the number of VUs increasing and decreasing, use the ramping VUs executor. We've already used the simplified syntax for this when using
stages
before, remember? In total, you need 5 stages to simulate the scenario described. Use 40startVUs
. Run the test and confirm that everything still works.
-
For scenario 3 we'll have 100 users continuously signing up for 1 hour (i.e., 5s for us). We will use the constant VUs executor. However, because we don't want to do this from the start, but during "lunch time" the executor needs to have a delayed start. For that we'll use
startTime
, one of the common scenario options, with a good example here.Create a function
lunchTime
that creates a single user, and alunchTime
scenario to execute the function. Run the test. Notice that our scenario is in awaiting
state with a countdown untilstartTime
time has passed.
-
Finally, for scenario 4, we're looking at a scenario with repeating spikes in traffic every "hour", typical for applications with batch jobs that run every hour/day/etc. There are no executors that directly support this repeated spike pattern, but we can use different methods to simulate it. We'll use the constant arrival rate executor again, with
rate = 1
andtimeUnit = 5s
deleting 20 users with a loop in the scenario function (what shouldduration
andpreAllocatedVUs
be, and why does this work?).We'll have to create the users to delete in
setup()
. Since scenario 2 also creates data in setup, we'll have to make sure they don't interfere with each other's test data. This can be achieved by returning an object like{ todoScenarioTodoListIds: number[], batchScenarioUserIds: number[] }
, each scenario reading their respective lists. Change thesetup()
function, and update scenario 2 to match.Implement the
batch
scenario function to use the correct array in the object parameter, and loop over the 20 next users. Use thescenario.iterationInInstance
execution environment variable to keep track of where you are in the array. Run the test to confirm that it's working.
Custom metrics, tags and groups give a lot of flexibility to measuring performance.
-
There are four types of custom metrics:
Counter
,Gauge
,Rate
andTrend
. Custom metrics are used to extend the set of built-in metrics. These are very useful to measure application- or business-specific metrics. We'll modify the test from task 1 (create-users.js
) to add aRate
.Take a look at the docs for
Rate
and add auser_creation_failed
metric that tracks how many user creations that fail. We also want a couple of thresholds for our new metrics: one verifying that the error rate is less than 10% at the end of the test, and one that aborts the test prematurely if the error rate exceeds 30%. Test your metric by reducing therandomIntBetween(...)
range used to create usernames. -
Custom metrics are useful, but in many cases using tags might be easier to filter (built-in or custom) metrics that already exist. Tags are used to categorize measurements. We've already seen some examples of built-in tags: the
http_reqs
counter metric has astatus
tag for the status code that was used for thresholds in a previous task. The documentation describes many ways tags can be used.Performance metrics can (and usually do) change with changes in load. With a helper library, each stage of a load test can be tagged, giving us insight into performance metrics for each stage. Take a look at the documentation for tagging stages. Use your code from task 2 (
create-todo-lists.js
) and add tags for each stage.Try running the program. Observe that the output does not contain our tags. This is currently known behavior, with discussions and open issues related to this. The workaround is to add the thresholds with empty arrays to the
options
object, in this case:thresholds: { 'http_req_duration{stage:0}': [], 'http_req_duration{stage:1}': [], 'http_req_duration{stage:2}': [], }
Run again to verify that it works as expected.
ℹ️ The results can be streamed real time as individual data points, including a JSON file which can be used to analyze the metrics with tags, without the workaround. Using the workaround is simpler in this workshop.
-
Groups are special tags, defined in a special way. Read through the documentation. We'd like to separate the main VU code from the
setup()
andteardown()
lifecycle functions. Add a group,main
, wrapping the main VU code, and a new threshold forhttp_req_duration{group:::main}
similar to the previous task. The:::
is not an error, but a filter using the::main
tag for the group. Run and verify that you get a separate metric for the new group in the summary.
For interactive testing, you can use the externally controlled executor. This executor can be used to scale up or down, pause and resume a test. Read about the externally controlled executor and this blog post about controlling it. Modify the create-users.js
test from previous tasks to use an externally controlled executor.
If you want to run the app locally, outside a Docker container, you need npm and Node 18. If you're using nvm
to manage Node, run nvm use 18
. If you're using brew
, brew install node@18
installs both.
- Spin up the database in a container: From anywhere, run
docker run -e "ACCEPT_EULA=Y" -e 'MSSQL_SA_PASSWORD=k6-workshop!' -p 1433:1433 -d --name k6-workshop-database --rm mcr.microsoft.com/azure-sql-edge:latest
to start a database in a container in the background. (After the workshop: Rundocker stop k6-workshop-database
to stop and remove the database.) - From the repository root folder, run
npm ci
, followed bynpx prisma migrate dev
andnpm run dev
to start the demo app.