Sessions
18 minute read
Each test case has to have at least one session, which describes what a newly launched client will do. Every client that is launched will pick a session based on its configured likelihood.
Every session has
- a descriptive
name
and - a
step definition function
name
is an identifier which you can freely choose but it has to be unique within one test case definition.
Within the step definition function
the actual steps of a session are defined.
Example:
definition.session("new sign up", function(session) {
session.get("/");
session.post("/sign-up", {
payload: { email: "foo@example.com" }
});
// ...
});
definition.session("guest", function(session) {
// ...
});
Session Likelihood
There are two ways to configure the likelihood of a session. It describes how likely a session is going to be picked up by a newly started client.
If you don’t specify anything explicitly, we assume that all sessions have an equal weight of 1, so each session is equally likely to be picked up.
Weights
You can configure the likelihood of a session to be picked up by weights. A session with weight of 3 is 3 times as likely to be selected compared to a session with weight of 1.
If you do not provide weights at all, a weight of 1 is assumed for all sessions. To deactivate a session, you can set its weight to 0 or comment it out.
definition.setSessionWeights({
"new sign up": 3,
"guest": 1,
"disabled session": 0,
// "also disabled: 42,
});
Note:
- At least one session with a non-zero weight must exist.
- The default weight of sessions is 1.
- Use the Arrival Phase
session_weights
option to overwrite per phase.
Probabilities
The total sum of all probabilities has to be exactly 100.
definition.setSessionProbabilities({
"new sign up": 94.5,
"guest": 5.5
});
Steps
The step definition function’s parameter is used to define session steps.
The step definition function will operate on the given argument session
.
Despite basic request steps you can implement simple control structures like wait times, conditions or loops:
session.get("/ping");
session.wait(0.5);
session.waitUniform(10, 20);
session.times(4, function(context){});
session.if(variableName, comparator, value, function(context){});
session.forEver(function(context){});
To see all available request types check out the request reference.
NOTE: times
, if
and forEver
statements must have at least one sub-step.
Helper Functions
To simulate think times of users in a session, the following helper functions are available wait
, waitExp
and waitUniform
.
wait
wait
allows you to specify a static think time in seconds. To wait exactly 20.5 seconds, use
session.wait(20.5);
Static wait times can be used if you know that your API client does wait for a fixed period of time.
If you want to simulate human think time behaviour, please check out waitExp
.
waitExp
In the example above, the think time will be an exponential distribution with a mean equal to 23.1 seconds. This means that the think time varies within the probability distribution that describes the time between events.
session.waitExp(23.1);
We recommend to use waitExp
for human think times.
waitUniform
To simulate think times of users in a more uniform way, you can use the waitUniform
function with a min/max value.
session.waitUniform(0.9, 10);
The distribution will be uniform in the interval (in the example above) from 900 milliseconds to 10 seconds.
times
To model a loop you can use the times
function.
session.times(4, function(context){});
Note that the callback function accepts a context
that must define the steps to run as part of the loop.
doWhile
While session.times()
loops for a number of times, sometimes you need a bit more flexibility:
session.doWhile()
allows running the block repeatedly while the condition is true.
session.doWhile(function(context) { /* .. */ }, variable, comparator, value);
Note that unlike times()
here the first parameter is the stepFunction
, as it will always be executed once. The condition comes afterwards.
This allows repeating the stepFunction
based on results from the function execution itself.
Valid comparator
s are: =
, !=
, >
, >=
, <
and <=
— for equal, not equal, greater than, greater or equal than, less than and less than or equal.
Here is an example:
// Not shown: Create a job and extract job id into variable "jobid"
session.doWhile(function(context){
context.wait(5);
context.get("/job/:jobid/status", {
params: {
"jobid": session.getVar("jobid"),
},
extraction: { jsonpath: {
"status": "$.job.status",
}}
});
}, session.getVar("status"), "!=", "done");
This snippet performs a request to read the status of a job. The status
variable is extracted and used to check if the job has already reached the done
state. We use !=
here, since we don’t want to repeat the loop, if the state is equal to done
. Note that we use wait()
here as well to simulate some wait time.
Note that the callback function accepts a context
that must define the steps to run as part of the loop.
if
For conditional statements the if
function.
session.if(variableName, comparator, value, function(context){});
Valid comparator
s are: =
, !=
, >
, >=
, <
and <=
for equal, not equal, greater than, greater or equal than, less than and less than or equal.
Example:
session.if(session.getVar("myVariable"), ">", 2, function(context) { });
session.if(session.getVar("otherVariable"), "!=", "", function(context) { });
Note that the callback function accepts a context
that must define the steps to run as part of the condition.
forEver
If you need an endless loop you can use the forEver
function. The loop will terminate when your defined test duration time is over.
session.forEver(function(context){});
Note that the callback function accepts a context
that must define the steps to run as part of the loop.
Transactions
Transactions are a helper to measure the durations of multiple requests as a logic unit of execution. They are reported in the test run details.
session.transaction("outer", function(ctx) {
ctx.get("/first/request");
ctx.transaction("inner", function(ctx) {
ctx.get("/inner");
})
ctx.get("/last/request");
});
The example above has one outer
transaction containing three requests. The middle request is also wrapped in a separate inner transaction.
This will count the request duration of the /inner
request towards the inner
and outer
session.
Note that the callback function accepts a context
that must define the steps to run as part of the transaction.
NOTE: StormForge also automatically creates a transaction at the top level of every session definition. Transaction data for sessions is shown separately in the report as “Session Timings”.
Session random branching
To select a different branch in a more random way, you can use session.chooseByWeight()
, session.chooseByProbability()
, or session.withProbability()
.
Branches will be selected based on their weight or probability.
chooseByWeight
session.chooseByWeight([
[1.0, function(c) { c.get("/choose/1x"); }],
[1.5, function(c) { c.get("/choose/1.5x"); }],
[2.0, function(c) { c.get("/choose/2x"); }],
]);
chooseByProbability
session.chooseByProbability([
[10, function(c) { c.get("/choose/10-percent"); }],
[25, function(c) { c.get("/choose/25-percent"); }],
[65, function(c) { c.get("/choose/65-percent"); }],
]);
session.withProbability(25, function(c) { c.get("/choose/25-percent-request"); });
Note that chooseByProbability
must not have a sum larger than 100 between all probabilities.
One branch will be picked from the various alternatives.
Also, the callback function accepts a context
that must define the steps to run as part of the branch.
withProbability
session.withProbability(25, function(c) { c.get("/choose/25-percent-request"); });
session.withProbability(session.getVar("myVariable"), function(c) { c.get("/choose/dynamic"); });
withProbability
will run the given branch with the specified probability.
In the example above, the /choose/25-percent-request
request is performed for 25% of the sessions.
Note that every session.withProbability()
statement has a separate chance to be selected, so having multiple branches in the same session might run various combinations of branches.
Also, the callback function accepts a context
that must define the steps to run as part of the branch.
Session Options
You can set additional options for a specific session. Available are:
Rate limiting
Rate limiting allows controlling the bandwidth of each client for all requests that follow this option.
You can specify both the download (ingress) and upload (egress) rate by calling session.withOption("rate_limit", option)
.
The options
parameter supports the following fields:
Field | Description |
---|---|
ingress |
Sets the ingress rate limit in kibibytes per second (1 kibibytes = 1024 bytes) |
ingress_burst |
Sets the maximum burst value for ingress traffic (in kibibyte) |
egress |
Sets the egress rate limit in kibibyte per second |
egress_burst |
Sets the maximum burst value for egress traffic (in kibibyte) |
reset |
If set to true resets both the ingress and egress rate limiting. Must not be used with the ingress or egress option |
rate |
Deprecated: Alias for the ingress parameter |
burst |
Deprecated: Alias for the ingress_burst parameter |
Example:
// Limit download transfer rate to 1024 kibibytes/sec and upload rate to 256 kibibytes/sec
session.setOption("rate_limit", {
ingress: 1024,
egress: 256,
});
// Only configure the download transfer rate to a typical 16 mbit DSL connection
session.setOption("rate_limit", { ingress: ((16*1024)/8) })
If you only need rate limiting for a few requests, you can reset the rate request as well. Subsequent requests won’t be rate limited.
// Resets both the ingress and egress rate limit
session.setOption("rate_limit", { reset: true });
Deprecated Rate Limiting API
The following parameters were previous iterations on the rate limiting API and are now deprecated.
The configure the ingress
rate limiting only and are not recommended for new test cases.
// Ratelimit of 1 megabyte per second
session.setOption("rate_limit", {
rate: 1024,
});
session.setOption("rate_limit", { rate: ((16*1024)/8) }); // A typical 16 mbit DSL connection
// To reset a currently active rate limiting, set `rate` to the string `"unlimited"`.
session.setOption("rate_limit", {
rate: "unlimited",
});
TLS Client Certificates
You can use TLS client certificates e.g. for strong authentication with your application.
To specify the certificate, private key and password for private key, you can use the certificate
option:
session.setOption("certificate", {
"cert": session.ds.getRawFile("tls_client_certificate.pem"),
"key": session.ds.getRawFile("tls_client_key.pem"),
"key_password": "password",
});
Note that you have to provide the certificate and private key in PEM-encoded format as data source type raw
, see data sources reference.
If you need to provide the certificate or private key more dynamically, you can also use cert_data
and key_data
respectively to pass PEM encoded data to setOption()
:
session.setOption("certificate", {
"cert_data": session.getVar("certificate"),
"key_data": "-----BEGIN PRIVATE KEY-----\n...",
"key_password": "password",
});
Subsequent requests for this client will use this certificate for new TLS connections.
Cookie Domain Mapping
Note
This feature is currently in alpha and the API is subject to change. We would love to receive feedback.Cookie Domain Mapping allows you to treat multiple domains as one with regards to cookie storage. This is useful when testing directly against your load-balancers or backends while skipping the CDN. In this case you might have to test against multiple target domains which in the production environment are served under one domain.
The cookie_domain_map
option accepts a list of domains that should get rewritten when the client sees a Set-Cookie
response header (without a specific domain
part).
session.setOption("cookie_domain_map", {
"upstream-backend.example.com": "www.example.com",
});
session.post("http://upstream-backend.example.com/login", { payload: "example-payload" });
session.get("http://www.example.com/my/profile"); // will see the cookie set by /login
This example configures a cookie domain map where the upstream-backend.example.com
domain is treated as www.example.com
in the cookiejar.
Any Set-Cookie
header received by the post()
request is later available for all further requests to the upstream-backend.example.com
and www.example.com
domain.
This also works the other way around as any future requests to upstream-backend.example.com
will also use any cookies configured for www.example.com
.
Note that configuring a cookie_domain_map
shadows any preexisting cookies on the key domains (upstream-backend.example.com
in the example above).
DNS Mapping
Note
This feature is currently in alpha and the API is subject to change. We would love to receive feedback.DNS Mapping allows replacing the target of a request with a different hostname or IP address. This is useful in various situations:
- The system under test has a different public DNS entry than used internally and configured on the load balancers
- The origin instead of the CDN should be tested
- The system under test has no public DNS record (but is available over an IP address and requires the
Host
header) - The system under test is only available over the IP address but requires a hostname for the TLS handshake (for Server Name Indication)
To configure DNS Mapping call session.setOption()
with dns_map
and a mapping of the domain names:
definition.setTargets(["https://www.example.com", "https://www2.example.com", "https://10.0.0.1", "https://origin.example.com"]);
session.setOption("dns_map", {
"www.example.com": "10.0.0.1",
"www2.example.com": "origin.example.com",
});
session.get("https://www.example.com/"); // request will be sent to 10.0.0.1
session.get("https://www2.example.com/"); // request will be sent to origin.example.com
When establishing a connection, the load generator will check the DNS Mapping.
If the request hostname is found as a key in the mapping, it will instead connect to the configured replacement.
The request itself is not changed (e.g. the Host
header remains intact).
Note that both the request target and the mapped target need to be configured in the targetlist.
TLS Version Pinning
When performing requests over HTTPS each client selects the newest TLS version (TLS 1.3 at the moment).
If you need to limit the selectable TLS versions, you can configure the tls_version
option to mimic older mobile clients or applicances that do not have access to TLS 1.3.
session.setOption("tls_version", {
min: "1.0",
max: "1.2",
});
session.get("https://example.com/", { tag: "tls_12_request" });
This example will select TLS 1.2 (instead of 1.3) when performing the TLS handshake with the server to negotiate the TLS version (assuming the server provides TLS 1.2).
The following values are allowed for min
and max
:
1.0
(default formin
)1.1
1.2
1.3
(default formax
)
Note that the platform limits will supersede any TLS version settings in session options. That is, you cannot pin a TLS version that is outside of the supported TLS versions for your environment.
Please contact us if you have specific TLS requirements not covered by these configuration options.
Variables
Each sessions have a set of variables.
Besides some pre-defined variables, you can add your own via Content Extraction or session.setVar()
(see below).
To access a variable, use session.getVar(varName)
. This returns a placeholder that can be used to represent the value.
If you need to print all variables with their current state, use session.dumpVars()
. Checkout our debugging guide as well.
The placeholder can be used in all places where you actually want to send the value. It will be replaced during the test run execution with the actual value. See the Javascript Runtime for more details.
Pre-Defined Variables
Each session comes with set of pre-defined variables which you could use e.g. for logging or debugging purposes. Pre-defined variables can be accessed the same way other custom variables are accessed (see Content Extraction).
session.get("/ping?=" + session.getVar("client_id"));
Each session provides the following variables:
- client_id: An unique ID for each client per session
- test_run_uid: The unique ID for a test run instance (e.g.
FQJpH0sA
) - test_run_id: A sequenced integer ID scoped per test case
- test_case_uid: The unique UID for a test case (e.g.
1mkpVJNC
) - test_case_name: The test case name given by the user (e.g.
black_friday_scenario
)
Custom Variables defined via Content Extraction have precedence over Pre-Defined variables.
Custom Variables
You can also define your own variables with session.setVar(varName, value)
:
session.setVar("address", "New York, New York")
Calculating dynamic values for variables
Note
Note: This feature is currently in beta. We would love to receive feedback.Since our DSL is not a direct scripting language, you cannot run arithmetic expressions to calculate dynamic values inside a session with pure JavaScript.
calcInt()
and calcFloat()
are provided to allow for dynamic calculation of values on the fly instead:
var factor = session.ds.define("random_number", {name: "factor", range: [1, 15]});
var offset = 15123;
var bid = session.tools.calcInt("bidPrice", "%f * (1 + (%d / 100)) + %f", [
session.getVar("price"), // first %f placeholder
session.ds.generateFrom(factor), // %d placeholder
offset // second %f placeholder
]);
session.post("/", {
payload: JSON.stringify({
bidPrice: session.getVar("bidPrice")
})
});
calcInt()
evaluate the given expression during a loadtest with the actual values and returns the result formatted as an integer (rounded); calcFloat()
returns a float
respectively.
The expression
must be an arithmetic expression, which supports:
numbers
(integer and float literals)- positional arguments:
%f
for float,%d
for integers - parentheses:
(1+(%f/100)
You can use %d
and %f
to define positional arguments to the expression.
These provided arguments MUST be either a numerical literal or a variable (this includes datasources).
For variables, the values are transformed to an interger (rounded) or float, if necessary.
Session Checks (OK/KO criteria)
You can set different counter for assertions or checks on your response data and (if needed) you can cancel sessions based on a criteria.
Checks
You can use session.check()
to define OK/KO criteria and every match or mismatch will be counted and shown in the reportings.
In the following example will everything with an HTTP status code above 400 counted as KO
and everything else as OK
:
session.get("/products");
session.check("products_check", session.lastHttpStatus(), "<=", 400);
The first parameter is the name of the check, next the first value, the comparator (see all comparators here) and the last value.
For more details, check out our Checks & Assertions Guide.
Assertions
You can use assertions with session.assert()
.
The assertions are almost like the checks. But if the KO
criteria matches, the session will be aborted.
Usage example:
session.get("/products");
session.assert("product_assert", session.lastHttpStatus(), "<=", 400);
This assertion causes a session abort, if the status is (in the example) above 400
.
Any session abort will be counted and is shown in the reports and any mismatch will be shown as OK
.
For more details, check out our Checks & Assertions Guide.
Session Aborts
Session aborts can be used on different ways and are mainly used to abort a session for a matching criteria. All session aborts will be counted and are shown in the reports.
Abort as session method
You can use session.abort()
to abort a session based on a given criteria, for example:
session.get("/token", {
extraction: {
jsonpath: {
"accessToken": "authorization.token",
}
}
});
session.if(session.getVar("accessToken"), "=", "", function(c) {
c.abort("no_access_token");
});
Abort as request option
You can use abort_on_error
as option on your requests to abort the session, if the response HTTP status code is 400
or higher.
For example:
session.get("/products", {
abort_on_error: true
});
If you want to disable/enable all abort_on_error
you have defined in your session, you can use a default setting:
session.defaults.setAbortOnError(true);
Cookie Jar Settings
The cookie jar API enables you to set cookies that will persist for the duration of a session, delete cookies that have been set manually or by remote servers, or reset the cookie jar and clear all set cookies.
This API provides three functions to perform these tasks, all of which exist in the cookies
sub-object of the session
object: session.cookies.add( options )
, session.cookies.delete( options )
, and session.cookies.reset()
.
Adding cookies
session.cookies.add( options )
adds cookies to a session that will persist for the duration of the session unless deleted or reset.
The options
parameter supports the following fields:
Field | Description |
---|---|
name |
Cookie name. Required. |
domain |
Cookie domain. Required. |
value |
Cookie value. Optional. |
path |
Cookie path. Optional. Defaults to “/” |
secure |
Security setting. Boolean, acceptable values are true and false . Optional, defaults to false . Secure cookies will not be sent on unsecure (http) connections. |
Deleting cookies
session.cookies.delete( options )
deletes cookies from a session. These cookies may have been added manually using session.cookies.add( options)
, or set by a remote server during an earlier http(s) request.
The options
parameter supports the following fields:
Field | Description |
---|---|
name |
Cookie name. Required. |
domain |
Cookie domain. Required. |
path |
Cookie path. Optional. Defaults to “/” |
Please note that in order to sucessfully delete a cookie, all of the options
parameters must match the cookie you are trying to delete.
If delete is called with incorrect parameters or called on a nonexistent cookie, nothing will happen.
Resetting the cookie jar
session.cookies.reset()
clears the cookie jar, where all cookies for a session are stored. A reset will delete all cookies that have been set, whether manually or by a remote server.
Example
definition.session("cookiejar_example1", function(session) {
// Add a cookie to the cookie jar for the session
session.cookies.add({
domain: "testapp.loadtest.party",
name: "foo",
value: "bar",
});
// Check to see if we set the cookie successfully
session.get("http://testapp.loadtest.party/cookie/get?cookie=foo", {
tag: "cookiejar_add_check",
});
// Delete the cookie we added
session.cookies.delete({
domain: "testapp.loadtest.party",
name: "foo",
});
// Alternatively, you could use the reset function to delete all set cookies
session.cookies.reset();
});
Functions
Note
The current function invocation pattern is a preview and may change in the future.Some additional functions are available via the session.invokeFunction(name, options)
helper. See below for details.
Function: Timestamp
The timestamp
function allows to get the current unix timestamp (UTC) and store it in a variable.
session.invokeFunction("timestamp", {output: "ts"});
session.get("/?ts=:timestamp", {
params: {
"timestamp": session.getVar("ts"),
}
});
This will send a request to /?ts=1576502911
.
Function: UUID4
The uuid4
function generates a 128-bit long unique identifier in the form of five groups (8-4-4-4-12
) of hexadecimal characters:
session.invokeFunction("uuid4", { output: "uuid4_result" });
session.get("/?uuid=:uuid", {
params: {
"uuid": session.getVar("uuid4_result"),
}
});
This will send a request to /?uuid=5e31d2c6-6e44-4776-acce-2187eb1b8717
.
Functions: Base64, URL, Query and HTML Encoder/Decoder
The following table list a number of functions that can be used with invokeFunction()
that all map an input
to an output
variable.
Name | Comment |
---|---|
base64_encode |
Encodes the input as a base64 string |
base64_decode |
Decodes the base64 input string |
html_escape |
Replaced all html tokens to be safe inside other HTML |
html_unescape |
Decodes a html_escape d string |
query_escape |
Escaped more characters to be safely used in a URL query or a form-encoded payload |
query_unescape |
Decodes a query_escape d string |
url_escape |
Escaped various characters to be safely used in an URL, e.g. the / , but not + or = |
url_unescape |
Decodes a url_escape d string |
All functions listed here take an input
option, transform it and store it in the variable defined by output
.
Here is an example for base64_encode
:
// base64_encode
session.setVar("foo", "World");
session.invokeFunction("base64_encode", {
input: `Hello, ${session.getVar("foo")}!`,
output: "b64_encode_result",
});
session.check("fun_b64_encode", session.getVar("b64_encode_result"), "=", "SGVsbG8sIFdvcmxkIQ==");
Function: ISO Date
The function format_timestamp_as_iso_date
takes a unix timestamp and formats it as an ISO 8601 / RFC 3339 date string.
Parameters timestamp
and output
are required.
timezone
is optional and defaults to the UTC
timezone.
timestamp
and timezone
can be dynamic.
session.invokeFunction("format_timestamp_as_iso_date", {
timestamp: "1604330278", // Unix timestamp (required)
timezone: "CET", // IANA Timezone, e.g. UTC (default), Europe/Berlin, CET, CEST (optional)
output: "resultVarName", // Output variable (required)
});
The example above will store 2020-11-02T16:17:58+01:00
in the variable resultVarName
and can be accessed via session.getVar("resultName")
.
Here is an example that calculates an ISO date 1 hour into the future:
session.invokeFunction("timestamp", {output: "current_timestamp"});
session.tools.calcInt("future_timestamp", "%d + (60 * 60 * 24)", [session.getVar("current_timestamp")]);
session.invokeFunction("format_timestamp_as_iso_date", {
timestamp: session.getVar("future_timestamp"),
output: "future",
});
session.log("Calculated future date: " + session.getVar("future"));
Functions: HMAC (SHA1, SHA256)
We support two functions to generate a HMAC: hmac_sha1
and hmac_sha256
.
const key = "3031323334353637383941424344454630313233343536373839414243444546" // 0123456789ABCDEF0123456789ABCDEF hex encoded
session.invokeFunction("hmac_sha1", {
message: "Hello World",
hexkey: key,
output: "sha1_hmac_result",
});
session.assert("hmac_sha1_check", session.getVar("sha1_hmac_result"), "=", "43ac45386f88434faa76f98972e2ddfe42bb7c4b");
session.invokeFunction("hmac_sha256", {
message: "Hello World",
hexkey: key,
output: "sha256_hmac_result",
});
session.assert("hmac_sha256_check", session.getVar("sha256_hmac_result"), "=", "41a8df467d07c735dcbcaa2f2ef3937aefc8346325c567f79f7f654cbcb432eb");
// Use output_format to get base64 instead of hex encoded output
session.invokeFunction("hmac_sha256", {
message: "Hello World",
hexkey: "012345", // STILL hex encoded!
output_format: "base64",
output: "sha256_hmac_result",
});
session.assert("hmac_sha256_check", session.getVar("sha256_hmac_result"), "=", "Uz0HW/SSlDCaGBrgvU4FtSX0ZRy1H7PjL/nxjpjTdpY=");
This examples shows both function accepting three parameters: message
, hexkey
and output
. The message
is the input to the HMAC function, while the hexkey
is a hex encoded representation of the key data. output
is the variable that will be used to store the result, hex or base64 encoded. To use the result (e.g. in requests) use session.getVar()
.
You can also control the output encoding via the optional output_format
parameter (defaults to hex
), which you can set to hex
or base64
.
Functions: SHA256
If you need to hash some input, you can use the sha256
hash function. If you need other algorithms, tell us!
session.invokeFunction("sha256", {
message: "Hello World!",
output: "hashout",
});
session.assert("sha256_check", session.getVar("hashout"), "=", "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069");
You can also control the output encoding via the optional output_format
parameter (defaults to hex
), which you can set to hex
or base64
.