Load Testing with Gatling

Can Cizer
Devjam
Published in
11 min readSep 22, 2021
gatling.io

Enterprise applications require not only functional approval, but performance checks as well. So, Gatling has come up with a Load Testing solution that helps you to foresee the slowdowns and crashes of the applications under heavy traffic. Gatling is an open-source load testing tool that detects the performance issues of a web application by simulating virtual users. It is designed with the Load-Test-As-Code principle and can be used for the continuous load testing integrated with your development pipeline.

Features Of Gatling

The Gatling Load Testing tool is written purely in Scala code. With the straightforward and expressive DSL offered by Gatling, it is fairly easy to write load testing scripts simulating millions of virtual users navigating your application. You will not need a complex distributed testing infrastructure to allow for a huge amount of traffic — all it takes to run the script is a single computer. The Gatling code can be checked into a version control system and used seamlessly with the Continuous Integration tools to run load and performance tests as part of your CI build. The load test generates an HTML report presenting all the data and charts involved in the test.

Setup Gatling Project

Let’s initialize a project and see how the Gatling load test works. The Gatling tool requires JDK version 8 or later. For the purpose of this blog, I have chosen IntelliJ IDEA as my IDE, which comes with the built-in Scala support. I prefer to use Maven version 3.6.1 (any later version also works fine) as a build tool. You can also try the official Gatling Maven plugin and the Gatling Maven Plugin Demo project. Check the information on Maven plugin to get more detail.

There are two ways to initialize a project with Gatling — you can download it from the gatling.io website or use the Maven Archetype. I prefer using the Maven Archetype as it is a fairly easy tool to create a project.

mvn archetype:generate -DarchetypeGroupId=io.gatling.highcharts \
-DarchetypeArtifactId=Gatling-highchairs-maven-archetype \
-DgroupId=com.example \
-DartifactId=Gatling-load-test \
-Diversion=1.0-SNAPSHOT \
-DinteractiveMode=false

Please note that the “archetypeGroupId” must be io.gatling.highcharts and “archetypeArtifactId” must be gatling-highcharts-maven-archetype. The rest depends on your preferences.

After initializing the project, open it with Intellij IDEA. Once the project is imported, open the Engine.scala class, which is located in src > test > scala folder. If you see “No Scala SDK in module” message at the top, click on “Setup Scala SDK” and add the Scala SDK into the project.

No Scala SDK in Module Warning
Add Scala SDK

Tested API

The simulations that are implemented into the Gatling project will run performance tests on the “JsonPlaceHolder” web application that provides some fake data. You can read more about it at jsonplaceholder.typicode.com.

Gatling Scripting

To cover the core concepts of the Gatling script development, there will be several tests defined; each topic will have its own script class and can be run individually with the Maven command below.

mvn gatling:test \
-Dgatling.simulationClass=simulations.{SimulationClassName}

Let’s create a package under the scala folder with the name “simulations”, where all the test simulation classes will be stored.

Create Simulations package under scala folder

Gatling Script Basics

First, create a Scala class called “BasicSimulation” in the package simulations’ folder. Then, add the code below.

BasicSimulation.scala

Now, we will go through each part of the script.

Imports

There are four imports in the script;

import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._
import io.gatling.http.protocol.HttpProtocolBuilder

The core.Predef._ and http.Prefed._ libraries are required for the script to run, and the other two ScenarioBuilder and HttpProtocolBuilder are the type definitions of the objects scn and httpProtocol.

Gatling Simulation Class Extension

Each gatling test classes must extend the Simulation class.

class BasicSimulation extends Simulation {

The rest of the script is composed of three parts.

HTTP Configuration

Defining HTTP configuration is the first step of each gatling scripts. The basic configuration definition is the baseUrl where each request is sent to, with the header included in each HTTP call.

//1. HTTP Configuration  
val httpProtocol: HttpProtocolBuilder = http .baseUrl("https://jsonplaceholder.typicode.com") .header("Accept", "application/json")

For more detailed information, refer to the Gatling documentation for the HTTP protocol.

Scenario Definition

The scenario definition shows the steps that the virtual user will follow.

//2. Scenario Definition  
val scn: ScenarioBuilder = scenario("First Scenario") .exec(http("Get All Posts")
.get("/posts"))

In the example above, a GET request is sent to the endpoint https://jsonplaceholder.typicode.com/posts with the domain coming from the baseUrl info in the httpProtocol object. The texts in the scenario and http functions define the reason of the test, and the results, presented in the generated load testing report, are matched with these scenario definitions.

Load Scenario

The last step of the script is setting a load profile, including the number of the virtual users and the duration of the test.

//3. Load Scenario  
setUp(
scn.inject(atOnceUsers(1))
).protocols(httpProtocol)

The example above involved a single virtual user created with a single iteration. The definitions are inserted inside the scn object, and the httpProtocol is set as the protocol.

Pause and Check Status

Let’s add more functionality into the script.

PauseAndCheckStatusSimulation.scala

The structure of the new script is similar to the previous one, but this time there are three different API calls with added pause and check methods. A pause causes a break between the requests and check, as its name implies verifies the response code of the HTTP request.

  • .pause(5) // waits for 5 seconds
  • .pause(1, 10) // waits for a random time between 1 and 10 seconds
  • .pause(2000.milliseconds) // waits for 2000 milliseconds

To use milliseconds definitions, import scala.concurrent.duration.DurationInt into the script.

  • .check(status.is(200))) // verifies 200 response code
  • .check(status.in(200 to 210))) // verifies that the response code something between 200 to 210
  • .check(status.not(404), status.not(500)) // verifies that the status code is not a 404 or 500

Correlations

Other than status code check, it is sometimes required to assert some expected data in the response and extract it from there. “Check Functionality” can be used with JsonPath to correlate the specific data and to reuse it.

Alternatively, you can also use JmesPath.

CorrelationSimulation.scala

On line 21, the title field of the first post is verified against the value defined inside the is method. On line 26, id field of the second post is extracted to the variable postId. Then on the lines 30 and 31, the extracted variable postId is used to retrieve the content of the second post from the API with its title field is corresponding to the value inside the is method, which is “qui est esse”.

Code Reuse

Sometimes, an API call need to be made more than one time. In such cases, instead of writing the same call one after the another, it is more efficient and elegant to define a method and call it as many times as required.

CodeReuseSimulation.scala

In the example above, the getAllComments and getAllPosts methods were implemented to call to retrieve the comments and posts data from the API and repeat function is used to state the number of times the calls must be made. After that, all that is left to do is to put the name of the method calls in the scenario builder, and that’s it!

CSV Feeder

The test data can be injected into the gatling script dynamically using the csv feeder. All that it takes is to create a csv file under the folder src > test > resources and import it into the script. The following example uses the csv file postCsvFile.csv under the folder src > test > resources > data and includes the data below.

postId,postTitle
1,sunt aut facere repellat provident occaecati excepturi optio reprehenderit
2,qui est esse
3,ea molestias quasi exercitationem repellat qui ipsa sit aut
CsvFeederSimulation.scala

On line 17, the csv file is inserted into the script with the circular option, following which the csvFeeder object is set to the feed method when the request is defined. There are different options available as well;

  • .queue // default behavior: use an Iterator on the underlying sequence
  • .random // randomly pick an entry in the sequence
  • .shuffle // shuffle entries, then behave like queue
  • .circular // go back to the top of the sequence once the end is reached

On line 25 and 26, the variables ${postId} and ${postTitle} are retrieved from the imported csv file circularly.

Custom Feeder

Test data can also be generated dynamically by using a template file containing the placeholders for the values. Custom data is required especially when creating and updating request bodies.

CustomFeederSimulation.scala

Between the lines 18 and 23, there are some helper variables and methods implemented to generate random custom values.

//Generates iterated values between 11 and 20
var idNumbers: Iterator[Int] = (11 to 20).iterator
//Generates a limited string with randomly generated characters
val rnd = new Random()
def randomString(length: Int): String = { rnd.alphanumeric.filter(_.isLetter).take(length).mkString
}

Random value generators are used to create the custom feeder iterator. Each field is mapped to a custom value generator, and then the customFeeder is set to the feed in the “request creation” step.

//3. Custom feeder mapper  
val customFeeder: Iterator[Map[String, Any]] = Iterator.continually(Map(
"userId" -> idNumbers.next(),
"title" -> ("Title-" + randomString(5)),
"body" -> ("Body-" + randomString(6)),
))

To inject the values into the customFeeder generator, it is necessary to define the request body structure and set it to the body method with the json option. This json file is created under the folder src > test > resources with a name like postNewPost.json.

Different subfolders can be created under the resources folder as in the code

The variable names in the json file must match the ones in the customFeeder.

{
"userId": "${userId}",
"title": "${title}",
"body": "${body}"
}

ElFileBody method is used to set the json file as the request body

Load Simulation

So far, we have injected only one virtual user into the test scenario for each simulation. However, more users are usually expected to access our API; so, using the load simulation setup, the test script can behave to simulate the scenario when there are more than one users trying to access the API.

BasicLoadSimulation.scala

The most important part of the example script above is the step 4. Load Scenario with some different definitions. This is how the virtual users behave in this scenario;

  • nothingFor(5 seconds) // Upon executing the Gatling script, do nothing for 5 seconds initially
  • atOnceUsers(5) // Start up 5 users at the same time
  • rampUsers(10) during (10 seconds) // Start a further 10 users over a period of 10 seconds

As a result, you get the Gatling test output where multiple virtual users are created and injected into the test;

Virtual users injected in time

To use the seconds duration postfix, scala.concurrent.duration._ and scala.language.postfixOps packages must be imported into the script.

Ramped User

There are also more advanced ways of injecting virtual users into the load test script.

RampUsersLoadSimulation.scala

This time constantUsersPerSec and rampUsersPerSec methods are used to create the virtual users. Here is the description of how it works;

  • constantUsersPerSec(rate) during(duration) // Injects users at a constant rate, defined in users per second, during a given duration. The users will be injected at regular intervals.
  • rampUsersPerSec(rate1) to (rate2) during(duration) // Injects users from a starting rate to a target rate, defined in users per second, during a given duration. The users will be injected at regular intervals.

If you use randomized keyword in both examples, the users will be injected at random intervals.

The graph of the injected users as shown in the result of the test;

Ramped virtual users during a given time

Fixed Duration

The duration of the test depends on how long the virtual user’s requests take. The test ends once all iterations are completed. However, it is also possible to set a fixed duration of the test. In this case, virtual users’ requests loop continuously until reaching the end of the fixed time or until the remaining iterations stop once the time is up.

FixedDurationLoadSimulation.scala

To set the simulation duration to a fixed time, first wrap the calls to a forever block and then set the duration value inside the maxDuration method, just like on line 52. The test example above will last only for 1 minute.

Number of Active users during the fixed time

Assertions

Use assertions to validate further conditions at the end of the test. Failure of any of the defined assertion conditions leads to the failure of the build.

AssertionsSimulation.scala

The assertion conditions are defined inside the .assertions function attached to setUp block. In the example above, there are two conditions, global.responseTime.max.lt and global.successfulRequests.percent.gt. If the maximum time of all responses exceeds 500ms or the percentage of all successful requests is less than 95%, then the test and the build fail. For more detailed information and definitions of various condition, please check the reference on assertions.

Command Line Parameters

The values in the simulation test do not need to be defined statically. Depending on the test environment or any other condition, the values can be injected into the test using the command line parameters.

RuntimeCommandParametersSimulation.scala

As a fallback, a helper method is defined as in the example where the parameter is retrieved from the system environment; otherwise, the script will use the default value.

//1. Helper method to get the property or the default value  
private def getProperty(propertyName: String, defaultValue: String): String = {
Option(System.getenv(propertyName)) .orElse(Option(System.getProperty(propertyName))) .getOrElse(defaultValue)
}

Between lines 21 and 25, the values of the parameters are set to variables; to verify the exact value, they are printed inside the “before” block as on line 28. After that, these values can be replaced with the ones that were defined statically.

The next step is to run a Gatling Maven command with the parameters required for the script. Type “-D” and write the name and the value of the parameter. That’s it!

mvn gatling:test \
-Dgatling.simulationClass=simulations.RuntimeParameters \
-DUSERS=10 \
-DRAMP_DURATION=5 \
-DDURATION=30

Reports

The Gatling test generates a report that includes all load testing data related to each scenario defined in the script. The location of the report is given at the end of the build. (the report files are usually located under the target > gatling folder).

Reports generated in 0s.
Please open the following file: /sytac/devblog/gatling/gatling-load-test/target/gatling/fixeddurationloadsimulation-20210831180606995/index.html

You can see an example report below; more information can be found in the reference link reports.

Gatling Test Report

Conclusion

As you can see from the examples, Gatling is a fairly easy tool for load testing simulations. It does not require very advanced Scala knowledge, and the DSL offered by Gatling is straightforward and expressive enough to be able to write the scripts.

For more information and references, please check the links below;

Below is the GitHub repository link that includes all the examples used in this blog.

https://github.com/ccizer/gatling-load-test

--

--