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 begatling-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.
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.
Gatling Script Basics
First, create a Scala class called “BasicSimulation” in the package simulations’ folder. Then, add the code below.
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.
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.
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.
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
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.
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.
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;
To use the
seconds
duration postfix,scala.concurrent.duration._
andscala.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.
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;
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.
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.
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.
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.
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.
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.