Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworks "life cycle" explanation page #624

Closed
wants to merge 15 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
---
title: 'Test life cycle'
excerpt: 'The four distinct life cycle stages in a k6 test are "init", "setup", "VU" and "teardown".'
excerpt: 'The four distinct life cycle stages in a k6 test are "init", "setup", "VU", and "teardown".'
---

The four distinct life cycle stages in a k6 test are "init", "setup", "VU" and "teardown"
Throughout the documentation, you will also see us referring to it as "init code", "VU code" etc.
A k6 test has four distinct stages, which always run in the same order:

1. *Init code* initializes VUs
2. *Setup code* sets up data and the test environment
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved
3. *VU* code runs the test
4. *Teardown* cleans up data and stops the test environment

MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved

<CodeGroup labels={["The four life cycle stages"]} lineNumbers={[true]}>

Expand All @@ -26,10 +31,26 @@ export function teardown(data) {

</CodeGroup>

## A quick overview

The rest of this article dives into the technical details of each stage.
This table provides a more high-level picture.

| Test stage | Used to | Example | Called | Required? |
|-------------|----------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------|-----------|
| **1. init** | Load local files, import modules, declare global variables | Open JSON file, Import module | Once per VU\* | No |
| **2. Setup** | Set up data for processing, share data among VUs | Call API to start test environment | Once | No |
| **3. VU code** | Run the test script | Make https requests, validate responses | Per VU, for as many iterations and as long a duration as the test options required | Yes |
| **4. Teardown** | Process result of setup code, stop test environment | Validate that setup had a certain result, send webhook notifying that test has finished | Once per script | No |

\* In cloud scripts, init code might be called more often.



## Init and VU stages

Scripts must contain, at the very least, a `default` function - this defines the entry point
for your VUs, similar to the `main()` function in many other languages:
Scripts must contain, at the very least, a `default` function.
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved
The `default` function defines the entry point for your VUs, similar to the `main()` function in other languages:

<CodeGroup labels={["Default/Main function"]} lineNumbers={[true]}>

Expand All @@ -41,33 +62,45 @@ export default function () {

</CodeGroup>

_"Why not just run my script normally, from top to bottom"_, you might ask - the answer is: we
do, but code **inside** and **outside** your `default` function can do different things.
Why not just run my script normally, from top to bottom?

Code inside `default` is called "VU code", and is run over and over for as long as the test is
running. Code outside of it is called "init code", and is run only once per VU.
This is a good question, and the answer is: we do.
But code *inside* and *outside* your `default` function do different things.

VU code can make HTTP requests, emit metrics, and generally do everything you'd expect a load
test to do - with a few important exceptions: you can't load anything from your local filesystem,
or import any other modules. This all has to be done from the init code.
Code inside `default` is called *VU code*.
VU code runs over and over through the test duration.
Besides setup and teardown, code outside of `default` is *init code*.
It runs only once per VU.

We have two reasons for this. The first is, of course: performance.
VU code can make HTTP requests, emit metrics, and generally do everything you'd expect a load test to do.
There are a few important exceptions. VU code:
* *Does not* load files from your local filesystem,
* *Does not* import any other modules.

If you read a file from disk on every single script iteration, it'd be needlessly slow; even
if you cache the contents of the file and any imported modules, it'd mean the _first run_ of the
script would be much slower than all the others. Worse yet, if you have a script that imports
or loads things based on things that can only be known at runtime, you'd get slow iterations
thrown in every time you load something new.
Instead of VU code, init code does these jobs.

But there's another, more interesting reason. By forcing all imports and file reads into the
init context, we make an important design goal possible; we want to support three different
execution modes without the need for you to modify your scripts; local, cloud and clustered
execution. In the case of cloud and clustered execution we know which files will be needed, so
we distribute only those files. We know which modules will be imported, so we can bundle them
up from the get-go. And, tying into the performance point above, the other nodes don't even
need writable filesystems - everything can be kept in-memory.
Why separate init and VU code? There are two chief reasons:
* Better performance
* Better design

As an added bonus, you can use this to reuse data between iterations (but only for the same VU):
### Performance is faster and more accurate
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved

If you read a file from disk every iteration, the script will be needlessly slow.

Even if you cached the contents of the file and any imported modules, the _first iteration_ of the script would be much slower than all subsequent iterations.
Worse, if your script imports or loads data dynamically, you'd get slow iterations each time you loaded something new.

### Design is more modular

Besides performance, there's another, more interesting reason to separate init code from VU code.
By forcing all imports and file reads into the init context, k6 lets you execute in different modes without modifying your scripts.

You should be able to run a k6 script in local, cloud, and clustered systems.
In the case of cloud and clustered execution, we know which files are needed, so we distribute only those files.
We know which modules to import, so we can bundle them from the get-go.
And, tying into the performance point above, the other nodes don't even need writable filesystems&mdash;everything can be kept in-memory.

As a bonus, you can use this to reuse data between iterations (but only for the same VU):

<CodeGroup labels={[]}>

Expand All @@ -83,29 +116,27 @@ export default function () {

## The default function life-cycle

A VU will execute the default function from start to end in sequence. Nothing out of the ordinary
so far, but here's the important part; once the VU reaches the end of the default function it will
loop back to the start and execute the code all over.
A VU executes the default function from start to end in sequence.
Once the VU reaches the end of the default function, it loops back to the start and executes the code all over.

As part of this "restart" process, the VU is reset. Cookies are cleared and TCP connections
might be torn down, depending on your test configuration options.
As part of this "restart" process, k6 resets the VU.
Cookies are cleared and TCP connections
might be torn down (depending on your test configuration options).

> Make sure to use `sleep()` statements to pace your VUs properly. An appropriate amount of
> sleep/think time at the end of the default function is often needed to properly simulate a
> user reading content on a page. If you don't have a `sleep()` statement at the end of
> the default function your VU might be more "aggressive" than you've planned.
> Make sure to use `sleep()` statements to pace your VUs properly, simulating a user reading content on your page.
> If you don't have a `sleep()` statement at the end of the default function, your VU might be more "aggressive" than you've planned.
>
> VU without any `sleep()` is akin to a user who constantly presses F5 to refresh the page.
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved

## Setup and teardown stages

Beyond the required init and VU stages, which is code run for each VU, k6 also supports test-wide
setup and teardown stages, like many other testing frameworks and tools. The `setup` and
`teardown` functions, like the `default` function, needs to be exported functions. But unlike
the `default` function `setup` and `teardown` are only called once for a test. `setup` is called
at the beginning of the test, after the init stage but before the VU stage (`default` function),
and `teardown` is called at the end of a test, after the VU stage (`default` function). Therefore,
VU number is 0 while executing the `setup` and `teardown` functions.
Like `default`, `setup` and `teardown` functions must be exported functions.
But unlike the `default` function, k6 calls `setup` and `teardown` only once per test.

* `setup` is called at the beginning of the test, after the init stage but before the VU stage.
* `teardown` is called at the end of a test, after the VU stage (`default` function).

The VU number is 0 while the `setup` and `teardown` functions execute.
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved

Again, let's have a look at the basic structure of a k6 test:

Expand All @@ -129,19 +160,18 @@ export function teardown(data) {

</CodeGroup>

You might have noticed the function signature of the `default` function and `teardown` function
takes an argument, which we here refer to as `data`.
You might have noticed the function signatures of the `default` and `teardown` functions
take an argument, referred to here as `data`.

This `data` will be whatever is returned in the `setup` function, so a mechanism for passing data
from the setup stage to the subsequent VU and teardown stages in a way that, again, is compatible
with our goal of supporting local, cloud and clustered execution modes without requiring script
changes when switching between them. (it might or might not be the same node that runs the setup
and teardown stages in the cloud or clustered execution mode).
This `data` is whatever the `setup` function returns.
Having a mechanism for passing data from the setup stage to the subsequent VU and teardown stages is compatible with our goal of supporting local, cloud, and clustered execution modes without requiring script changes when switching between them.
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved
In cloud and clustered modes, the node that runs the setup code might not be the node that runs the teardown code.

To support all of those modes, only data (i.e. JSON) can be passed between `setup()` and the
other stages, any passed functions will be stripped.
To support these modes, you can pass only data (i.e. JSON) between `setup()` and the
other stages.
You cannot pass functions.

Here's an example of doing just that, passing some data from setup to VU and teardown stages:
Here's an example of passing some data from setup to VU and teardown stages:

<CodeGroup labels={["Setup/Teardown"]} lineNumbers={[true]}>

Expand All @@ -163,9 +193,8 @@ export function teardown(data) {

</CodeGroup>

A big difference between the init stage and setup/teardown stages is that you have the full k6
API available in the latter, you can for example make HTTP requests in the setup and teardown
stages:
You can call the full k6 API in the setup and teardown stages, unlike the init stage.
For example, you can for make HTTP requests:

<CodeGroup labels={["Setup/Teardown with HTTP request"]} lineNumbers={[true]}>

Expand All @@ -188,14 +217,14 @@ export default function (data) {

</CodeGroup>

Note that any requests made in the setup and teardown stages will be counted in the end-of-test
summary. Those requests will be tagged appropriately with the `::setup` and `::teardown` values
Note that the [the end-of-test summary](/results-visualization/end-of-test-summary/#summary-export-to-a-json-file) includes requests made in the setup and teardown stages.
These requests are tagged appropriately with the `::setup` and `::teardown` values
for the `group` metric tag, so that you can filter them in JSON output or InfluxDB.

## Skip setup and teardown execution
MattDodsonEnglish marked this conversation as resolved.
Show resolved Hide resolved

It is possible to skip the execution of setup and teardown stages using the two options `--no-setup` and
`--no-teardown` respectively.
You can skip the execution of setup and teardown stages using the options `--no-setup` and
`--no-teardown`.

<CodeGroup labels={["Skipping setup/teardown execution"]} lineNumbers={[true]}>

Expand Down