Gatling: Simulation Scripts Parameterization
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:
- Feeders to inject values taken from a static file,
- Regular Expression extractors to extract value(s) from a server response and inject it in subsequent requests,
- Cookies management.
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 to make it more realistic (download it here). Indeed, this first version is only load testing a single page of the sample PetStore:
This web page contains links to the categories of the PetStore (the different kinds of pet).
In the HTML code of the page, 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")
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/two/
folder.
In the simulation script, declaring a CSV Feeder is done using the csv
keyword:
val csvFeeder = csv("two/categories.csv").random
Here the csv feeder is assigned 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:
Distributed Load and File Splitting¶
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.
- 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 host-1 host and categories2 on the host-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 | host-3.
Then, in the Gatling script you inject the environment variable with the following syntax:
val categoriesSetId = System.getProperty("CATEGORIES_SET_ID")
val csvFeeder = csv("two/" + categoriesSetId + ".csv").random
Create One File Per Injector¶
You need to manually split the categories.csv file in two, with the following content.
categoryId
BIRDS
FISH
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).
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 policysetUp(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.
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:
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.
You can download the complete script here.
That looks good! Debugging this script in 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.
Want to go further? You can update the script to extract item IDs (from the HTML of the product pages) and make our 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:
jsonPath()
: To extract value(s) from a JSON response,css()
: To extract value(s) from an HTML body using CSS selectors,xpath()
: To extract value(s) from an XML document using XPath 1.0 expressions.
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.
- Open a new Incognito Window on your web browser,
- Go to the PetStore: https://petstore.octoperf.com/,
- Open the developers Console (F12 on Chrome and FireFox) and head to the Network tab,
- Click on the Enter the Store link in the web page,
- In the Network tab of the console, open the Catalog.action request,
- You will see the HTTP response header
set-cookie: JSESSIONID=BDF1B88FEEBD0AC4460CC1B6E0C83CAD; Path=/; HttpOnly
, it sets the JSESSIONID Cookie value, - Click on the Fish Category link,
- In the Network tab of the console, open the last Catalog.action request,
- Its request URL should be
?viewCategory=&categoryId=FISH
, - 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 a Grafana report shows us how many request are sent for each page:
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. Check out this blog post to learn more about loops and pauses.