Gatling: Simulation Scripts Parameterization

Gatling: Simulation Scripts Parameterization

Kraken   Give Your Opinion

This blog post is a tutorial for writing Gatling scripts to load test web applications. It follows our first getting started with Gatling simulation scripts article.

The application under test is a fake e-commerce. We are going to create a Virtual User that browses articles in this shop. To create a dynamic load test we will cover several topics:

Prerequisites

Gatling Basics

Since this blog post is the second of a series, you are expected to know the basics of writing a Gatling simulation, with:

  • The structure of the simulation Class,
  • The HTTP protocol configuration,
  • Writing a very basic scenario to make a single GET request,
  • Load injection profiles,
  • Running Gatling.

HTTP Overview

You also need to know the base principles of the Hypertext Transfer Protocol and how is built an HTTP request.

HTTP Query Parameters

Let’s focus on the Query Parameters (or Query String) as they are going to be set using Gatling’s DSL in this guide.

A typical URL containing a query string is as follows:

https://petstore.octoperf.com/actions/Catalog.action?viewCategory=&categoryId=DOGS

When a server receives a request for such a page, it may run a program to generate the HTTP response. The query string follows the question mark ? character and is passed to the underlying program. Here, query parameters are separated by the ampersand & character:

  • viewCategory=: is a simple key viewCategory without value,
  • categoryId=DOGS: is a key/value pair that defines the ID of the category to view.

The returned HTML changes with the given parameters even though the path of the resource /actions/Catalog.action does not. For example, open the URL https://petstore.octoperf.com/actions/Catalog.action?viewProduct=&productId=K9-PO-02 and you will see a different page rendered.

Stateless Protocol

HTTP is a stateless protocol. HTTP servers do not need to store information or status about each user for the duration of multiple requests. Put another way, there is no link between two requests being sent on the same connection.

Left like that it would be problematic for many web applications such as our sample e-commerce. There must to be a way to store information about a visitor while he browses the shop and add articles to his cart! In fact there are several:

  • HTTP Cookies: it’s a piece of data sent from the server, stored on the user’s web browser and sent back with every subsequent request while the user is browsing,
  • Local storage: it’s a JavaScript object that stores data with no expiration date (unlike the sessionStorage that is cleared when the browser tab is closed),
  • XMLHttpRequests and the javascript fetch() API: these JavaScript tools allow modern Single Page web application to send requests to the server without reloading the page (states can then be stored in JS objects).

The JPetStore uses a JSESSIONID Cookie to store the user session identifier.

GET Request and Query Parameters

Enough theory! Let’s continue what was initiated in the first blog post and improve the created script (download it here) to make it more realistic. Indeed, this first version is only load testing a single page of the sample PetStore:

PetStore Homepage

This web page contains links to the categories of the PetStore (the different kinds of pet):

Kraken Debug Homepage

In the HTML code above, you can see that links are represented by the <a href="link-url">link name</a> tag. The URL of the page changes when the visitor clicks on one of those links.

For instance, if the visitor clicks on the Fish category, the page is reloaded with the new URL https://petstore.octoperf.com/actions/Catalog.action?viewCategory=&categoryId=FISH. The query parameters are:

  • viewCategory=: instructs the server to return the HTML for viewing a category,
  • categoryId=FISH: tells the server what category to view.

If you click on a product, the query string will change for ?viewProduct=&productId=FI-SW-01 and if you click on an item it will change for ?viewItem=&itemId=EST-1. So updating query parameters in our Gatling simulation allows us to simulate the behavior of a visitor that browses articles in the shop.

In a Gatling .scala Simulation script, you set the query parameters either by appending then manually at the end of the request path:

http("Homepage").get("/Catalog.action?viewCategory=&categoryId=FISH")

Or a better solution is to use the queryParam operator:

http("Homepage").get("/Catalog.action")
    .queryParam("viewCategory", "")
    .queryParam("categoryId", "FISH")

The viewCategory parameter has no value, but you still need to pass an empty string "".

Note:

Both keys and values of query parameters have to be URL encoded. You can use this online URL encoder to do it manually or use the java.net.URLEncoder: .queryParam("key", java.net.URLEncoder.encode("value to encode", "UTF-8")).

Values Feeders

But we are not going to copy/paste GET requests for every Category/Product/Item of the PetStore. It’s easier to maintain a dynamic load testing script when the tested application is updated (for example with new products in the case on an e-commerce).

For the sake of this tutorial we are going to use a Feeder. For a real test, it would be better to extract the value from the homepage HTML like we will do in the next chapter. Usually, Feeders are mostly used to generate values that cannot be extracted from server responses, such as user credentials.

In Gatling, a Feeder is an object that iterates over a list of values and feeds it to a scenario execution.

CSV Feeder Declaration

There are many kinds of Feeders in Gatling, from a simple array to a JDBC reader.
We will focus on the CSV Feeder, but feel free to write a comment if you would like explanations on another Feeder in particular.

key1,key2,key3
record1 value1,record1 value2,record1 value3
record2 value1,record2 value2,record2 value3
record3 value1,record3 value2,record3 value3

A Comma-Separated Values file is a delimited text file that uses a comma , to separate values. Each line of the file is a data record.

For Gatling, the first line defines the name of each column. Only subsequent lines are fed to the scenario. Let’s create a simpler CSV file named categories.csv that contains only on pet shop categories:

categoryId
BIRDS
FISH
DOGS
REPTILES
CATS

In Gatling, copy this file in the <GATLING_HOME>/user-files/resources/ folder. In Kraken, you can directly create a text file in the Resources tree:

Kraken CSV Resource File

In the simulation script, declaring a CSV Feeder is done using the csv keyword:

val csvFeeder = csv("categories.csv").random 

Here the csv feeder is affected to the csvFeeder variable val csvFeeder = and will be read randomly .random. The random suffix defines the reading strategy. There are several strategies available:

  • .queue: the default value if nothing is written after .csv(), it reads each line one after the other,
  • .random: reads a random line when a value is generated,
  • .shuffle: shuffles all lines then reads them one by one,
  • .circular: reads lines one by one and restarts at the top of the file when the end is reached.

Warning:

If you use the .queue or .shuffle strategies and your CSV file has not enough values to feed every iteration of your scenario, Gatling will stop the simulation execution!

Note:

A Comma-Separated Values file uses a comma , to separate values. The same principle can be used with different separators, for example, a semi-colon ; or a tab character \t.

In Gatling scripts, specific Feeders are dedicated to each use case:

  • val tsvFeeder = csv("categories.tsv") for tabulations,
  • val ssvFeeder = csv("categories.ssv") for semicolons.

You can even use a custom separator with the syntax val feeder = separatedValues("categories.txt", '/').

Feeder Usage

Time to use our feeder. The feeder is added to the execution chain of the scenario with the .feed keyword. You can then use Gatling’s Expression Language to inject values anywhere you want.

For instance, our CSV file contains the categoryId column and is configured with the random strategy. So you can inject a random category with the ${categoryId} string:

val scn = scenario("PetStoreSimulation")
    .exec(http("Homepage").get("/actions/Catalog.action"))
    .feed(csvFeeder)
    .exec(http("Catalog ${categoryId}")
        .get("/actions/Catalog.action")
        .queryParam("viewCategory", "")
        .queryParam("categoryId", "${categoryId}"))

The complete simulation script is downloadable  here:

Kraken PetStore CSV Feeder

In Kraken, you can debug your Gatling simulation to check which requests are sent:

Kraken PetStore Categories Debug

Here we can see two requests (I deactivated resources inferring for this debug run to display only the requests to the HTML pages):

  • The Homepage request,
  • A request to the category Fish page.

The request URL is visible at the upper left corner of the Debug editor. Click on the button on top of the response body to open it in a new browser tab. You can now check that the server answered with the right HTML content:

Kraken PetStore Categories Body

CSS and gifs are not displayed but it’s still the same as the PetStore Fish Category:

Kraken PetStore Fishes

This page shows various links to each fish product. Read the Variables Extractors chapter to learn how to extract the product IDs from the HTML and inject them in the next request.

Distributed Load and File Splitting

Using the Kubernetes version of Kraken you can easily start Gatling load tests from several injectors at the same time. All resources and simulations are copied on each host before the execution. So every injector will use a duplicated categories.csv file to generate categories. You may want to use a different set of values for each load injector.

For instance with two injectors and our PetStore, we will try to have a different set of categories visited depending on the injector:

  • BIRDS and FISH for one injector,
  • DOGS, REPTILES and CATS for the other.

Setup Environment Variables

Using environment variables allows Gatling to know what values should be used. The idea here is to inject an identifier of the categories when running the load test, let’s say CATEGORIES_SET_ID. In Kraken, environment variables are set when executing the simulation:

Kraken PetStore Fishes

  • Key: Defines the environment variable name,
  • Value: Defines the environment variable value,
  • Scope: The ID of a specific host or Global*` if the environment variable is used on every injector.

Here, the env variable CATEGORIES_SET_ID will have the value categories1 on the kraken-1 host and categories2 on the kraken-2. If you have more that two hosts you could easily add a row to define the Categories Set ID for it. For instance, we could run the test on three hosts and add a line CATEGORIES_SET_ID | categories2 | kraken-3.

Then, in the Gatling script you inject the environment variable with the following syntax:

val categoriesSetId = System.getProperty("CATEGORIES_SET_ID")

Create One File Per Injector

You need to manually split the categories.csv file in two, with the following content.

categories1.csv:

categoryId
BIRDS
FISH

categories2.csv:

categoryId
DOGS
REPTILES
CATS

Then you can easily load the appropriate file depending on the environment variable:

val categoriesSetId = System.getProperty("CATEGORIES_SET_ID")
val csvFeeder = csv(categoriesSetId + ".csv").random

Let’s run this simulation with 10 concurrent users on each injector and see how it goes (download the script here). We can see in the generated report that each injector ran its own set of ‘Categories’ requests:

Kraken Split CSV Request Tables

Note:

The same method can be used to have a different load injection strategy per host.

For example, setup the environment variable CONCURRENT_USERS and inject in with val load = Integer.getInteger("CONCURRENT_USERS", 42) (42 being the default value if the property is not set). Then use it in the injection policy setUp(scn.inject(atOnceUsers(load))).protocols(httpProtocol).

Variable Extractors

Let’s go one level deeper into the PetShop e-commerce and have our virtual user open a random Product page. This time we will extract available products from the server response instead of using a CSV Feeder.

There are many variables extractors in Gatling. In fact they are called Checks using the Gatling terminology. IMO, the most expandable is the Regular Expression Check. There are easier variable extractors for specific needs, but you can use the regexp no matter the type of content returned by the server: HTML, XML, JSON, etc.

Regular Expressions

Before looking at the syntax of Gatling scripts, we must learn a bit about Regular Expressions.

A regular expression (or “regex”) is a search pattern used for matching one or more characters within a string. It can match specific characters, wildcards, and ranges of characters.

I think that every load tester (and more generally every developer) should have at least a basic knowledge of Regexps and be able to write even simple patterns. Gatling being written in Scala, it uses the Java patterns format.

You can find more information about Regular Expression on Oracle’s tutorial. Even though the syntax is a bit different in JMeter, here are some example of Regex used in load testing scripts.

A few thing to remember for this tutorial:

  • The string .* matches any characters,
  • Parenthesis () are capturing groups,
  • Capturing groups allows you to extract a part of the matched character string.

Finally, it’s often quicker to copy/paste the server response in an online Regex tester to check that it works fine instead of running a load tests. There are several tools available:

Gatling’s Check and Regexp

Gatling’s .check keyword is used for two things:

  • Checking that the server response matches expectations (.ie returns a 2XX HTTP status),
  • Capturing some elements of the server response.

We will focus on the second usage here (A dedicated blog post will cover the first use case and assertions in general). You have to extract the product identifier from the HTML.

Here is a screenshot of Kraken’s debug editor for the ViewCategory server response body:

Kraken PetStore Fish Category Body

The product ID is present in the <a href=""> tag, between the string productId= and ". The regular expression in this case is productId=(.*)".

Let’s copy the HTML body in an online regexp testing tool to try it out:

Kraken Regexp Match Groups

As you can see all the product IDs are found.

In Gatling’s simulation script, use this regex as follows:

.exec(http("Catalog ${categoryId}")
            .get("/actions/Catalog.action")
            .queryParam("viewCategory", "")
            .queryParam("categoryId", "${categoryId}")
            .check(regex("""productId=(.*)"""").findRandom.saveAs("productId")))
        .exec(http("Product ${productId}")
            .get("/actions/Catalog.action")
            .queryParam("viewProduct", "")
            .queryParam("productId", "${productId}"))
  • .check is appended at the end of the GET request chain,
  • regex("""productId=(.*)"""") escapes the regular expression String by placing it between the """ characters,
  • .findRandom extract a random product ID (amongst other options you can use .findAll to extract a list of values or .count to count the matching tokens),
  • .saveAs("productId") stores the extracted value into Gatling Session,
  • .queryParam("productId", "${productId}")) gets the productId from the session using Expression Language and injects it in a query parameter.

Gatling Sessions:

A Gatling Session is a memory space dedicated to a Virtual User instance/iteration. You can store values on the fly in this Map in order to create a dynamic load test.

It is possible to print Session variables for debugging your scripts.

You can download the complete script here:

Kraken PetStore Simulation Regexp

That looks good! Debugging this script in Kraken shows us that three requests are executed (as long as the resources inferring is commented out of course):

  • The static Homepage,
  • A random Category page using the CSV Feeder,
  • A random Product page using our newly created Regexp Check.

Kraken PetStore Debug Product

Want to go further? You can update the script to extract item IDs (from the HTML of the product pages) and make ou virtual user go to a random one.

Other Extractors

Many values extractors are available in Gatling, they take the place of the regex keyword in the simulation script. Each one have a specific use case:

Just write me a comment if you need more information on one of these extractors.

Cookies

We saw earlier in this blog post that cookies are used to store the user session as HTTP is a stateless protocol.

Here is a simple test to view how Cookies are managed on the PetStore.

  1. Open a new Incognito Window on your web browser,
  2. Go to the PetStore: https://petstore.octoperf.com/,
  3. Open the developers Console (F12 on Chrome and FireFox) and head to the Network tab,
  4. Click on the Enter the Store link,
  5. In the Network tab of the console, open the Catalog.action request,
  6. You will see the HTTP response header set-cookie: JSESSIONID=BDF1B88FEEBD0AC4460CC1B6E0C83CAD; Path=/; HttpOnly, it sets the JSESSIONID Cookie value,
  7. Click on the Fish Category link,
  8. In the Network tab of the console, open the last Catalog.action request,
  9. Its request URL should be ?viewCategory=&categoryId=FISH,
  10. In the HTTP request headers you will see cookie: JSESSIONID=27AA1FFC6EB67A44DA79197426D2B141,

That is how the JSESSIONID Cookie set in the browser (with the set-cookie response header) and sent back to the server (using the cookie request header). This allows the PetStore server to track what page is visited by whom (and later on what item is added to whose cart).

What about Gatling? Once again it behaves like a web browser and handles Cookies transparently (like JMeter does). If you need some specific behavior it offers the following methods to manage the current Virtual User cookies:

  • exec(addCookie(Cookie("cookieName", "cookieValue"))): Adds a Cookie,
  • exec(getCookieValue(CookieKey("cookieName"))).saveAs("myCookie"): Saves the Cookie named cookieName in Gatling’s session at myCookie,
  • exec(flushSessionCookies): Flushes Session cookies (like when a user closes its web browser),
  • exec(flushCookieJar): Flushes all cookies.

Running the Load Test

Let’s roll! It’s time to run our simulation script with 100 concurrent users and see how it goes (The complete script is downloadable here):

setUp(scn.inject(constantConcurrentUsers(100) during(3 minutes))).protocols(httpProtocol)

Opening the Grafana report in Kraken shows us how many request are sent for each page:

Kraken PetStore No Thinktime

First of all, that’s a lot of requests (more than 150K all pages combined) for only a few concurrent users. The issue here is that we did not add any form of think-time when writing our script. We simulated users that would click on links without even taking the time to read the page (not mentioning the omitted browser rendering time). That is not realistic at all!

Also, the HomePage has 25K requests, 5 times more than the Category pages. This is also not realistic for an e-commerce: visitors would probably more often browse several categories and products without having to go back to the home page. We must use loops to simulate this path.

Stay tuned as both topics are the subject of my next blog post!

By - CEO.
Tags: Gatling Script Load Testing Http Query Parameter Feeder Csv Check Variable Extractor Regexp Cookies

Comments

 

Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK

Get our whitepaper,
A beginners guide to performance testing!