Testing HTTP


What you’ll learn

  • How to set configuration options specifically for testing HTTP services.
  • How to execute tests for different HTTP methods.
  • How to write and execute custom logic in Javascript for your tests.

HTTP-specific configuration

TLS/SSL

By default, Artillery will reject SSL certificates that it’s unable to validate. Typically, these errors occur when testing services with a self-signed certificate or an expired SSL certificate. You may see errors such as UNABLE_TO_GET_ISSUER_CERT_LOCALLY, UNABLE_TO_VERIFY_LEAF_SIGNATURE, CERT_UNTRUSTED or one of the other validation error codes when that happens.

To get around these issues when using Artillery, you can disable certificate validation with one of the following two options:

  1. Pass the -k or --insecure flag to the artillery run or artillery quick commands.
  2. Set rejectUnauthorized: false under the config.tls setting in your test script:
config:
target: "https://myapp.staging:3002"
tls:
rejectUnauthorized: false

Ignoring certificate errors can be useful for testing in a development or staging environment. However, you should never use this setting on a production environment. Ignoring certificate errors in a production system can lead to potential security vulnerabilities, like man-in-the-middle attacks.

Request timeouts

If a response to any request takes longer than 10 seconds, Artillery will abort the request and raise an ETIMEDOUT error.

To increase or decrease the default timeout period, set the config.http.timeout setting to the number of seconds for timing out the request.

config:
target: "http://my.app"
http:
# Responses have to be sent within 10 seconds, or an `ETIMEDOUT` error gets raised.
timeout: 10

Fixed connection pool

By default, Artillery opens a new connection for each new virtual user. To open and re-use a fixed number of connections instead, set the config.http.pool setting to the number of connections. This setting can help emulate the conditions when the target is behind a load-balancer and would have a fixed number of connections established at any given time.

config:
target: "http://my.app"
http:
# HTTP requests from all virtual users will be sent over the same ten connections.
pool: 10

Max sockets per virtual user

By default, Artillery creates one TCP connection per virtual user. To allow for multiple sockets per virtual user (to mimic the behavior of a web browser, for example), specify the number of connections with the config.http.maxSockets setting.

config:
target: "http://my.app"
http:
# Set a max of five TCP connections per virtual user.
maxSockets: 5

This setting is per virtual user, not for the total number of sockets. To limit the total number of sockets, use the config.http.pool setting.

Proxy support

Artillery can connect to the internet through a forward proxy, like a corporate proxy.

To send all traffic through a proxy, set the HTTP_PROXY environment variable to the proxy URL. The proxy URL itself may be HTTP or HTTPS. Both HTTP and HTTPS requests will be sent via the proxy.

HTTP_PROXY='http://my.proxy.app:3128' artillery run my-script.yaml

To send HTTPS traffic through a different proxy, set the HTTPS_PROXY environment variable. All HTTPS traffic will use this proxy.

HTTPS_PROXY='https://secure.proxy.app:3128' artillery run my-script.yaml

Scenario actions and configuration

This section describes functionality available in scenarios described in the scenarios section of a test script.

GET / POST / PUT / PATCH / DELETE requests

An HTTP request object may have the following attributes:

  • url - The request URL; will be appended to the target URL set in config.target but can also be a fully qualified URL.
  • json - A JSON object to send in the request body.
  • body - Arbitrary data to send in the request body.
  • headers - A JSON object describing header key-value pairs.
  • cookie - A JSON object describing cookie key-value pairs.
  • capture - Sse this to capture values from the response body of a request and store those in variables.

Example:

config:
target: "https://example.com"
phases:
- duration: 10
arrivalRate: 1
scenarios:
- flow:
- get:
url: "/"
- post:
url: "/resource"
json:
hello: "world"

Logging

You can print messages to the console for each scenario using the log action:

config:
target: "https://example.com"
phases:
- duration: 10
arrivalRate: 1
scenarios:
- flow:
- log: "New virtual user running"
- get:
url: "/"
- post:
url: "/resource"
json:
hello: "world"

In the example above, Artillery will print “New virtual user running” every time it generates a virtual user for the scenario.

The string argument to log may also include variables:

config:
target: "https://example.com"
phases:
- duration: 10
arrivalRate: 1
scenarios:
- flow:
- log: "Current environment is set to: {{ $environment }}"
- get:
url: "/"
- post:
url: "/resource"
json:
hello: "world"

Debug messages will get printed to the console even when running the tests in "quiet" mode (using the --quiet or -q flag with the run command).

Setting Headers

Arbitrary headers may be sent under the headers option for a request:

- get:
url: "/test"
headers:
X-My-Header: "123"

Compressed Responses (gzip)

Set gzip to true to have Artillery add an Accept-Encoding header to the request, and decode compressed responses encoded with gzip:

- post:
url: "/test"
json:
foo: bar
gzip: true

Basic Authentication

If your request requires Basic HTTP authentication, set your username and password under the auth option:

- get:
url: "/protected/resource"
auth:
user: myusername
pass: mypassword

Query Strings

Query strings can be appended directly to the url or set with qs:

- get:
url: "/products"
qs:
search_keyword: "coffee"
page_size: 25

The above request is identical to the following:

- get:
url: "/products?search_keyword=coffee&page_size=25"

Redirects

Artillery follows redirects by default. To stop Artillery from following redirects, set the followRedirect option on a request to false:

- get:
url: "/test"
followRedirect: false

Forms

URL-encoded forms (application/x-www-form-urlencoded)

Use the form attribute to send an URL-encoded form:

- post:
url: "/submit"
form:
name: "Homer Simpson"
favorite_food: "donuts"

Multipart forms (multipart/form-data)

Use the formData attribute to send a multipart/form-data form (forms containing files, non-ASCII data, and binary data).

- post:
url: "/upload"
formData:
name: "Homer Simpson"
favorite_food: "donuts"

To attach binary data, you can use a custom JS function.

First-class file upload support is also provided by Artillery Pro for teams requiring extensive file uploading functionality in their tests.

Extracting and re-using parts of a response (request chaining)

You can parse responses and re-use those values in subsequent requests using the capture option in your requests.

Syntax

To tell Artillery to parse a response, add the capture option to a request:

- get:
url: "/"
capture:
json: "$.id"
as: "id"

The capture option must always have an as attribute, which names the value for use in subsequent requests. It also requires one of the following attributes:

  • json - Allows you to define a JSONPath expression.
  • xpath - Allows you to define an XPath expression.
  • regexp - Allows you to define a regular expression that gets passed to a RegExp constructor. A specific capturing group to return may be set with the group attribute (set to an integer index of the group in the regular expression). Flags for the regular expression may be set with the flags attribute.
  • header - Allows you to set the name of the response header whose value you want to capture.
  • selector - Allows you to define a Cheerio element selector. The attr attribute will contain the name of the attribute whose value we want. An optional index attribute may be set to a number to grab an element matching a selector at the specified index, "random" to grab an element at random, or "last" to grab the last matching element. If the index attribute is not specified, the first matching element will get captured.

You can also optionally set the transform attribute, which is a string of JS code that transforms the value after it has been extracted from the response:

- get:
url: "/journeys"
capture:
xpath: "(//Journey)[1]/JourneyId/text()"
transform: "this.JourneyId.toUpperCase()"
as: "JourneyId"

In the example above, this refers to the context of the virtual user running the scenario (an object containing all currently defined variables, including the one extracted from the response).

Turn off strict capture

By default, captures are strict. If a capture rule fails because nothing matches, any subsequent requests in the scenario will not run, and that virtual user will stop making requests. This behavior is the default since subsequent requests typically depend on captured values and fail when one is not available.

In some cases, it may be useful to turn this behavior off. To make virtual users continue with running requests even after a failed capture, set strict to false:

- get:
url: "/"
capture:
json: "$.id"
as: "id"
strict: false # We don't mind if `id` can't be captured and the next requests 404s
- get:
url: "/things/{{ id }}"

Capturing multiple values

Multiple values can be captured in a single request with an array of capture specs:

- get:
url: "/journeys"
capture:
- xpath: "(//Journey)[1]/JourneyId/text()"
transform: "this.JourneyId.toUpperCase()"
as: "JourneyId"
- header: "x-my-custom-header"
as: "headerValue"

Examples

The following example sends a POST request to /pets to create a new resource, captures the ID of the new resource, and stores it in a variable called id. The variable is then used in the subsequent request to load the resource and checks if it matches the expected response.

- post:
url: "/pets"
json:
name: "Mali"
species: "dog"
capture:
json: "$.id"
as: "id"
- get:
url: "/pets/{{ id }}"
match:
json: "$.name"
value: "{{ name }}"

The following example grabs a matching a element at random and uses the value of its href attribute in the next request:

- get:
url: "/some/page"
capture:
- selector: "a[class^=productLink]"
index: "random"
attr: "href"
as: "productUrl"
- get:
url: "{{ productUrl }}"

Cookies

Cookies are remembered and re-used by individual virtual users. The cookie option can set custom cookies in individual requests.

The following example sets a cookie with the key saved with the value of tapir,sloth, which can be accessed in subsequent requests:

- get:
url: "/pets/"
cookie:
saved: "tapir,sloth"
- get:
url: "/pets/saved"

Pausing execution with think

To pause the virtual user for N seconds, use a think action:

- post:
url: "/pets"
json:
name: "Mali"
species: "dog"
capture:
json: "$.id"
as: "id"
#
# wait for 5 seconds:
#
- think: 5
- get:
url: "/pets/{{ id }}"
match:
json: "$.name"
value: "{{ name }}"

Conditional Requests

The ifTrue option can execute a request in a flow only when meeting a condition. ifTrue may take two forms:

  1. Set to a name of a scenario variable. If the variable is set, the request will be executed.
  2. Set to a simple conditional expression, which may refer to any of the scenario variables and use one or more of:
    • A numeric operation with +, -, *, /, % (modulo), or ^ (power).
    • A comparison with ==, <, >, <=, or >=.
    • A boolean operation with or, and, or not.

Examples

Make a GET request only if the keyword scenario variable is set to a value:

- get:
url: "/search?keyword={{ keyword }}"
ifTrue: "keyword"

Make a GET request only if the pageNumber scenario variable is less than 10:

- get:
url: "/pages/{{ pageNumber }}"
ifTrue: "pageNumber < 10"

Make a GET request only if the admin and user scenario variables are set to true:

- get:
url: "/admin"
ifTrue: "admin and user"

Loops

You can use the loop construct to repeat through several requests in a scenario.

Looping through a number of requests

You can loop through a specific number of requests using the count attribute.

The following example sends 100 GET requests to / for each virtual user:

scenarios:
- flow:
- loop:
- get:
url: "/"
count: 100

loop is an array - any number of requests can be specified. Variables, cookies, and response parsing will work as expected.

Accessing the loop count

You can access the current step of the loop by using the $loopCount variable, starting from 1. This variable is only available inside of the loop.

The following loop will first make a request to /pages/1, then /pages/2, and so on, up to /pages/100:

scenarios:
- flow:
- loop:
- get:
url: "/pages/{{ $loopCount }}"
count: 100

If the count option is omitted, the loop will run indefinitely.

Looping through an array

You can loop through an array of values by setting the over property to a specified array or by using the name of the variable containing an array of values. The loop can then access the value through the $loopElement variable.

In the following example, each value of the array defined in over will make one request for each value specified in the array, for a total of 3 requests:

scenarios:
- flow:
- loop:
- get:
url: "/products/{{ $loopElement }}"
over:
- id123
- id456
- id789

You can also iterate over different arrays defined as variables in the config section.

The following example uses the productIds variable inside of a loop. Each virtual user will make three requests for one of the arrays:

config:
target: "https://my.app.local"
phases:
- duration: 600
arrivalRate: 10
variables:
productIds:
- ["id1", "id2", "id3"]
- ["id4", "id5", "id6"]

scenarios:
- flow:
- loop:
- get:
url: "/products/{{ $loopElement }}"
over: productIds

In this example, a virtual user will make one of the following three requests in their loop:

  • /products/id1, /products/id2 and /products/id3
  • /products/id4, /products/id5 and /products/id6

Looping with custom logic

You can further control when to continue looping through a custom function defined in the whileTrue option.

For instance, let’s say you want to poll an endpoint until it returns a JSON response with the top-level status attribute set to "ready":

config:
target: "https://my.app.local"
phases:
- duration: 600
arrivalRate: 10
processor: "./my-functions.js"

scenarios:
- flow:
- loop:
- think: 5
- get:
url: "/some/endpoint"
capture:
- json: $.status
as: "status"
whileTrue: "statusReady"

The whileTrue option uses a JavaScript function loaded from my-functions.js containing the logic that determines when the loop stops:

module.exports = {
statusReady: statusReady
}

function statusReady(context, next) {
const continueLooping = context.vars.status !== 'ready';
// While `continueLooping` is true, the `next` function will
// continue the loop in the test scenario.
return next(continueLooping);
}

The whileTrue option takes precendence over count and over if either of those is specified.

Writing custom logic in Javascript

The HTTP engine has support for “hooks”, which allow running custom JS functions at specific points during the execution of a scenario.

  • beforeScenario and afterScenario - Called before/after a virtual user executes a scenario.
  • beforeRequest - Called before sending a request; request parameters like the URL, cookies, headers, and body can be customized here.
  • afterResponse - Called after a response has been received; the response can be inspected, and custom variables can be set here.

JS functions may also be run at any point in a scenario with the function action.

Loading custom JS code

To tell Artillery to load your custom code, set the config.processor setting to the path of your JS file:

config:
target: "https://my.app.dev"
phases:
- duration: 300
arrivalRate: 1
processor: "./my-functions.js"
scenarios:
- # ... scenarios definitions here ...

The JS file is expected to be a standard Node.js module:

//
// my-functions.js
//
module.exports = {
setJSONBody: setJSONBody,
logHeaders: logHeaders
}

function setJSONBody(requestParams, context, ee, next) {
return next(); // MUST be called for the scenario to continue
}

function logHeaders(requestParams, response, context, ee, next) {
console.log(response.headers);
return next(); // MUST be called for the scenario to continue
}

Function signatures

beforeRequest hooks

A function invoked in a beforeRequest hook should have the following signature:

function myBeforeRequestHandler(requestParams, context, ee, next) {
}

Where:

  • requestParams - Use this parameter to customize what to send in the request (headers, body, cookies, etc.).
  • context - The virtual user’s context. context.vars is a dictionary containing all defined variables.
  • ee - An event emitter that can be used to communicate with Artillery.
  • next - The callback which must be called for the scenario to continue. It takes no arguments.

afterResponse hooks

A function invoked in an afterResponse hook should have the following signature:

function myAfterResponseHandler(requestParams, response, context, ee, next) {
}

Where:

  • requestParams - Use this parameter to customize what to send in the request (headers, body, cookies, etc.).
  • response - Contains the response details (headers, body, etc.).
  • context - The virtual user’s context. context.vars is a dictionary containing all defined variables.
  • ee - An event emitter that can be used to communicate with Artillery.
  • next - The callback which must be called for the scenario to continue. It takes no arguments.

function actions and beforeScenario / afterScenario hooks

A function invoked as an action in a scenario definition, either through function, beforeScenario and afterScenario, should have the following signature:

function myFunction(context, ee, next) {
}

Where:

  • context - The virtual user’s context. context.vars is a dictionary containing all defined variables.
  • ee - An event emitter that can be used to communicate with Artillery.
  • next - The callback which must be called for the scenario to continue. It takes no arguments.

Specifying a function to run

beforeRequest and afterResponse hooks can be set in a request:

- post:
url: "/some/route"
beforeRequest: "setJSONBody"
afterResponse: "logHeaders"

In this example, Artillery runs the setJSONBody function before making the request and the logHeaders function after receiving the response.

Specifying multiple functions

You can also specify an array of function names, which run sequentially, one after another.

- post:
url: "/products"
beforeRequest:
- "setApiKey"
- "setJSONBody"
afterResponse: "logHeaders"

Setting scenario-level hooks

Similarly, a scenario definition can have beforeScenario and afterScenario attributes, which will make the specified functions run for every request in the scenario.

scenarios:
- beforeScenario: "setApiKey" # Run the function before each request.
afterScenario: "logResults" # Run the function after each request.
flow:
- get:
url: "/search?q={{ query }}"
- get:
url: "/products"

Function steps in a flow

A function may be run at any point in a scenario with a function action:

scenarios:
- flow:
- get:
url: "/search?q={{ query }}"
- function: "setupSomeData"
- get:
url: "/some/url?q={{ query }}"

JS functions invoked with function action have full access to the virtual user’s context:

function setupSomeData(context, events, done) {
// Set the "query" variable for the virtual user.
context.vars['query'] = 'foo';
return done();
};

Debugging

If you’re having issues getting your test scenarios to complete successfully, you can print out helpful debugging messages using the DEBUG environment variable.

On macOS and Linux systems, you can temporarily set the DEBUG environment variable by setting its value when running your Artillery test script:

DEBUG=http artillery run my-script.yaml

For the Windows Command Prompt, you first need to set the DEBUG environment variable using the set command before running the test script:

set DEBUG=http
artillery run my-script.yaml

If you use PowerShell on Windows, you can set the DEBUG environment variable using $Env:DEBUG before running the test script:

$Env:DEBUG = 'http'
artillery run my-script.yaml

The following examples use the macOS/Linux format for setting environment variables, but you can use any of the DEBUG values on Windows for your preferred command line interface.

Print request details, errors, and capture values

Set DEBUG=http when running your tests to view details about each request (URL, method, headers, etc.), values captured during a request, and any errors that occurred during the test run.

DEBUG=http artillery run my-script.yaml

Print request URL

Set DEBUG=http:request when running your tests to view the URL for each request made.

DEBUG=http:request artillery run my-script.yaml

Print response headers and body

Set DEBUG=http:response when running your tests to view each request’s response headers and body.

DEBUG=http:response artillery run my-script.yaml

Combining debugging messages

You can combine different debugging modes in a single test run by setting a comma-separated list of values for DEBUG. For instance, to print the request URL and the response headers and body:

DEBUG=http:request,http:response artillery run my-script.yaml

You can also display all HTTP debugging messages with DEBUG=http*:

DEBUG=http* artillery run my-script.yaml