Project Hurricane is a command line tool or web-based service for creating and scheduling load tests of HTTP based APIs. This tutorial will primarily cover using the web-based service. A specific command line related section is provided at the end. The tests are defined in JSON format and provide support for many different features:

  • Sequences of requests

  • Random selection from possible requests

  • Properties for defining the test environment

  • Variables for passing data between requests

  • Cookies

  • Custom HTTP headers

This article will guide the reader through the process of creating their first test script and provide examples for each type of script element available for use in their test scripts.

Table of Contents

Your First Script

When you first access the Hurricane service you will see the list of previously created test definitions.

Let’s create a new test:

  1. Enter a name for your test in the Name field at the bottom of the page

  2. Click the “Add Test Definition” button

Your test is created using the name you provided and you are redirected to the Edit Test Definition page.

For our first test, we’re just going to fill out the Main Script section. We’ll work with test properties and the initial and maintenance script documents later in this tutorial. Let’s start by adding some boiler plate content to the Main Script document:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "", 7 "type": "Request", 8 "method": "GET", 9 "url": "", 10 "headers": [], 11 "body": null, 12 "actions": 13 [ 14 ] 15 } 16 ] 17}

Let’s look at the top-level sections or our script:

  • headers - This array allows us to add custom global headers to our script that will be included when making API requests. These will be described later in this tutorial

  • steps - This array will hold the steps to take when running our test

Our first test will be very simple and make a single API request. Our boilerplate includes the body for a request step. Let’s look at the fields we’ll need to make an API request:

  • name - The name of this request. This name will help us differentiate between requests as our tests become more complicated

  • type - Since this is an API request the type is “Request”. Other types of steps are available and described later in this tutorial

  • method - The type of HTTP request being made. Can be GET, POST, PATCH, PUT, HEAD, or DELETE

  • url - The complete URL used to make the request. This includes the protocol http/https, server address, path, and any query parameters if needed

  • body - The body content to send as part of the request if using POST, PATCH, or PUT. This field should be set to null or removed if using any other method

  • actions - This array will hold actions to perform after the request has completed. Actions will be covered later in this tutorial

Let’s fill out the request fields with a call to a fictional API endpoint that returns “hello world!” when called:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello World", 7 "type": "Request", 8 "method": "GET", 9 "url": "http://localhost/hello", 10 "headers": [], 11 "body": null, 12 "actions": 13 [ 14 ] 15 } 16 ] 17}

Once we’ve made our changes, we need to save the test definition by clicking on the “Save” button at the bottom of the page. You will be returned to the list of Test Definitions where our newly created test is now listed:

For instructions on running your tests, please see How-to → Run a Load Test

Using Test Properties

Test properties allow you to create placeholders in your scripts that get replaced when the test is run. This makes it easy to create tests that can be run against multiple environments such as Dev, QA, or Production without having to change the test definition each time or have separate definitions for each environment.

Properties vs Variables

It's important to understand the differences between properties and variables and how they are used in a test script. The first difference is where the values for properties and variables come from. Property values are provided at test scheduling time, either through specifically providing a value or using the default value defined in the test definition. Variable values are extracted from request responses while the test is running.

The second difference is related to when the placeholders are replaced. Placeholders that reference properties are replaced prior to starting the test as part of the test preparation. This means that the values cannot be changed once the test has started. Variables change dynamically and their placeholders are replaced every time a test step is executed. Variables are also scoped so that each test session has its own copy of a variable and its value. The only exception to this is that variables used in the initial and maintenance scripts are also shared with all test sessions.

Add a Test Property

Let’s add a property to our test that will hold the protocol and server information for our API request. To add a test property:

  1. Click on the “Add” button in the Test Properties section of the Edit Test Definition page. This adds an empty property entry to the page

  2. Provide a name for the property. For this tutorial we’ll use “ServerDomain”. The name will be used as part of the placeholder and can only contain letters, numbers, ‘-', and '_’

  3. Optionally provide a default value to use for this property if a specific value isn’t given during test scheduling. For this tutorial we use our current server address: “http://localhost”

  4. Once added, click the “Save” button at the bottom of the page

Reference a Test Property

To use our new test property, we need to add a reference to it in our test’s main script. A property reference has the format {{propertyname}}. In our example, we change the “url” field of our request to include a reference to the ServerDomain property:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello World", 7 "type": "Request", 8 "method": "GET", 9 "url": "{{ServerDomain}}/hello", 10 "body": null, 11 "actions": 12 [ 13 ] 14 } 15 ] 16}

Just before the test is run, the {{ServerDomain}} portion of the “url” field will be replaced with the value of our property. In this case the “url” field value would be rewritten to “http://localhost/hello”. If the test was run against a QA environment, the value might be overwritten to “http://myqaserver”. Now our test can be pointed at any number of different servers at run time without having to change the test definition itself.

Not all fields support property and variable reference replacement. To see if a specific field supports this feature, please see the detailed documentation for each type of script element.

Nesting property references is not supported. The results will be indeterminate.

Sending a Request Body

Let’s create a new test definition using our boiler plate content. This time we’re going to send data to an endpoint that will echo back the text that was sent to it:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Echo", 7 "type": "Request", 8 "method": "POST", 9 "url": "{{ServerDomain}}/echo", 10 "headers": [], 11 "body": 12 { 13 "text": "My text content" 14 }, 15 "actions": 16 [ 17 ] 18 } 19 ] 20}

Short Form vs Long Form Body

The example above defines the body content using the short-form. Since the content is in Json format, the content can simply be written as the value of the “body” property. This also has the benefit that the Content-Type header will automatically be added when the test is run. However, if the content being sent isn’t in json format (i.e. XML) the long form must be used. The following script uses the long form to send the same body content:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Echo", 7 "type": "Request", 8 "method": "POST", 9 "url": "{{ServerDomain}}/echo", 10 "headers": 11 [ 12 { 13 "name": "Content-Type", 14 "value": "application/json" 15 } 16 ], 17 "body": "{\r\n\"text\": \"My text content\"\r\n}", 18 "actions": 19 [ 20 ] 21 } 22 ] 23}

Notice how special characters like line returns and quotes are escaped using ‘\'. If the content contains ‘\’ prior to being escaped, the ‘\’ is escaped by doubling up the '\’ to “\\”. Also, notice that the Content-Type header has to be explicitly added to the header section of this request (or could be added to the top-level header property that is inherited by all steps. Another consideration when deciding to use the short vs long form is the use of properties and variable placeholders. If the placeholder is contained in a quoted property name or value, the short form can be used without issue. However, if the placeholder is expected to be replace with structured content and the use of the placeholder is not quoted, the script definition will no longer be valid Json.

You do not need to provide the Content-Length header with your request body. The system will automatically calculate the length of the body after any property or variable replacements and add the header for you.

Making Multiple Requests

A test case may require making multiple requests. The top level “steps” property of the test definition is an array that can contain any number of test steps that will be executed in the order they appear when the test is run. Let’s combine our previous test definitions together:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello World", 7 "type": "Request", 8 "method": "GET", 9 "url": "{{ServerDomain}}/hello", 10 "body": null, 11 "actions": 12 [ 13 ] 14 }, 15 { 16 "name": "Echo", 17 "type": "Request", 18 "method": "POST", 19 "url": "{{ServerDomain}}/echo", 20 "headers": 21 [ 22 { 23 "name": "Content-Type", 24 "value": "application/json" 25 } 26 ], 27 "body": "{\r\n\"text\": \"My text content\"\r\n}", 28 "actions": 29 [ 30 ] 31 } 32 ] 33}

When this test is run, each session will make a call to the hello world endpoint followed by a call to the echo endpoint. After the second request returns, the session will start over and run both again until the test ends. Metrics for each request are tracked separately. Using the top-level “steps” array property makes it easy to run multiple requests in order. However, you may want separate groups of requests or other steps together to help organize a complex test. This can be done by using the sequence or random step which will be covered later in the tutorial.

Using Test Variables

Let’s update our test definition to capture the response from the hello world request and pass it to the echo request. First we need to capture the response and place it into a variable. We do this by adding a Json action to our first request:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello World", 7 "type": "Request", 8 "method": "GET", 9 "url": "{{ServerDomain}}/hello", 10 "body": null, 11 "actions": 12 [ 13 { 14 "name": "Capture Result", 15 "type": "json", 16 "extractionPairs": 17 [ 18 { 19 "jsonPath": "text", 20 "variableName": "myvar" 21 } 22 ] 23 } 24 ] 25 }, 26 { 27 "name": "Echo", 28 "type": "Request", 29 "method": "POST", 30 "url": "{{ServerDomain}}/echo", 31 "headers": [], 32 "body": 33 { 34 "text": "My text content" 35 }, 36 "actions": 37 [ 38 ] 39 } 40 ] 41}

All actions have these properties:

  • name - The name of the action

  • type - The type of the action (json, cookie). For this example, we’re using the json action to capture information from a json formatted response

The json action has an additional property:

  • extractionPairs - An array of path and variable name pairs that defines which json properties to extract and what variable to store them in

    • jsonPath - A dot separated list of json property names to extract

    • variableName - The name of the variable to store the value in. This name is then used in a placeholder in later requests

Our hello world endpoint returns a json document with a single property:

1{ 2 "text": "Hello World!" 3}

We reference this property in the jsonPath property. We also give our variable a name of “myvar” so we can use it later. Let’s go ahead and update our test definition to use our new variable to send “Hello World!” to the echo endpoint:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello World", 7 "type": "Request", 8 "method": "GET", 9 "url": "{{ServerDomain}}/hello", 10 "body": null, 11 "actions": 12 [ 13 { 14 "name": "Capture Result", 15 "type": "json", 16 "extractionPairs": 17 [ 18 { 19 "jsonPath": "text", 20 "variableName": "myvar" 21 } 22 ] 23 } 24 ] 25 }, 26 { 27 "name": "Echo", 28 "type": "Request", 29 "method": "POST", 30 "url": "{{ServerDomain}}/echo", 31 "headers": [], 32 "body": 33 { 34 "text": "[[myvar]]" 35 }, 36 "actions": 37 [ 38 ] 39 } 40 ] 41}

Information about the differences between Properties and Variables can be found here

Notice that the format of a variable reference is different from a property in that instead of surrounding the property name with “{{“ and “}}” you use “[[“ and “]]” for variables. When the session runs, it first makes the request to the hello world endpoint and then grabs the value from the “text” property of the response and stores it in the “myvar” variable. When the echo request is made, the placeholder reference “[[myvar]]” is replaced with the value stored in the “myvar” variable; “Hello World!” in our case.

Each time the session starts over, any variables captured during the previous run are removed to ensure nothing is carried over between loops. The only exception to this are variables captured or modified in the Initial and Maintenance scripts. Those variables are shared with all sessions and can only be modified by those scripts. If you attempt to update the value of a global variable within the Main script, an error will be raised. Once a variable is populated, it is available to all later requests during the same loop even if the request is part of a different group.

Grouping Requests Together

Although the Main script provides the top-level “steps” array that can hold multiple steps, it’s sometimes beneficial to explicitly group steps together. You can provide a name for the group and the system will track metrics of the group as a whole in addition to the metrics from each request.

Sequence Group

The first type of group is a Step Sequence and acts exactly like the top-level “steps” array. The steps defined in the group are executed in the order they appear. Let’s move our existing requests into their own sequence:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Hello and Echo", 7 "type": "Sequence", 8 "headers": [], 9 "steps": 10 [ 11 { 12 "name": "Hello World", 13 "type": "Request", 14 "method": "GET", 15 "url": "{{ServerDomain}}/hello", 16 "body": null, 17 "actions": 18 [ 19 { 20 "name": "Capture Result", 21 "type": "json", 22 "extractionPairs": 23 [ 24 { 25 "jsonPath": "text", 26 "variableName": "myvar" 27 } 28 ] 29 } 30 ] 31 }, 32 { 33 "name": "Echo", 34 "type": "Request", 35 "method": "POST", 36 "url": "{{ServerDomain}}/echo", 37 "headers": [], 38 "body": 39 { 40 "text": "[[myvar]]" 41 }, 42 "actions": 43 [ 44 ] 45 } 46 ] 47 } 48 ] 49}

The structure of the sequence group is very similar to the top-level script except the addition of:

  • name - The name of the group

  • type - The type of the group (Sequence, Random)

The top-level script now has a single step; our sequence group. When the sequence group step is executed, its children are then executed in order before the sequence step is complete. If we were to add another Request to our top-level script after the the sequence, the sequence (and its two children) would be completed prior to making the third request. Being able to run a number of steps in order as a group can help organize the test. But what if you want to add some randomness to the test?

Random Group

Similar to the Sequence group, the Random group step allows you to define a set of child steps. However, instead of running each step in order each time the group is executed the Random group will pick one of the children at random and run that step. Once the selected step is executed, the Random group is complete and the test continues on to the next step at the same level as the Random Group. Let’s look at script that uses a Random group:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Pick One", 7 "type": "Random", 8 "headers": [], 9 "steps": 10 [ 11 { 12 "name": "Call 1", 13 "type": "Request", 14 "method": "GET", 15 "url": "{{ServerDomain}}/call1", 16 "body": null, 17 "actions": 18 [ 19 ] 20 }, 21 { 22 "name": "Call 2", 23 "type": "Request", 24 "method": "GET", 25 "url": "{{ServerDomain}}/call2", 26 "headers": [], 27 "body": null, 28 "actions": 29 [ 30 ] 31 } 32 ] 33 }, 34 { 35 "name": "Hello World", 36 "type": "Request", 37 "method": "GET", 38 "url": "{{ServerDomain}}/hello", 39 "body": null, 40 "actions": 41 [ 42 ] 43 } 44 ] 45}

When this test is run, the Random group step is executed first. The group will pick either Call 1 or Call 2 to execute. The test then moves on to execute the Hello World step. During the next loop, the Random group will make another random selection.

Using the Initial Script

The Initial script is used to perform any setup tasks required by the Main script such as authentication or gathering data to use in variables. It is run prior to the Main and Maintenance scripts and unlike the other scripts, the initial script is only executed once. Let’s look at an example that uses the Initial script to perform authentication that will be used by the requests in the Main script:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Authenticate", 7 "type": "Request", 8 "method": "POST", 9 "url": "{{ServerDomain}}/login", 10 "body": 11 { 12 "username": "{{username}}", 13 "password": "{{password}}" 14 }, 15 "actions": 16 [ 17 ] 18 } 19 ] 20}

The login endpoint sets a cookie that contains the authentication token. This cookie will be sent as part of any requests made by the Main or Maintenance scripts automatically. Note that cookies set in the Initial and Maintenance scripts are global and shared with all sessions running the Main script. Unlike Main script cookies they will not be cleared between session loops. The Initial script is great for authentication, but what if that authentication will expire before the test has finished? Some tests might take several hours to complete. This is where the Maintenance script comes into play.

Using the Maintenance Script

The Maintenance script and delay are used to perform periodic actions that support the Main script. The platform will wait for the delay period before running the Maintenance script for the first time. Once it it complete, it will be run again after the same delay period. This loop will continue until the Main script has finished. In this example we’ll use the Maintenance script to refresh our authentication cookie every 10 minutes:

1{ 2 "headers": [], 3 "steps": 4 [ 5 { 6 "name": "Refresh Authentication", 7 "type": "Request", 8 "method": "GET", 9 "url": "{{ServerDomain}}/refreshlogin", 10 "body": null, 11 "actions": 12 [ 13 ] 14 } 15 ] 16}

In this case we don’t need to send any additional information because our /refreshlogin endpoint will receive the cookie that was set by the Initial script (it is shared globally) and set an updated cookie value during the response. The updated cookie is also shared globally so any new requests made by the Main script will use the new value.

The Maintenance Delay field is the amount of milliseconds between Maintenance script runs.