Writing realistic Gatling tests
In this tutorial, we assume that you have already gone through the
introductory guides and that you have a basic understanding of how a simulation works.
We will build a realistic load test for a relevant real-world scenario and introduce more advanced concepts and
Domain Specific Language constructs.
It is strongly recommended to review the introductory guides first, as this tutorial introduces more advanced concepts:
Additionally, it is important to have a basic understanding of a virtual user’s session. Kindly consult the Session documentation, particularly the Feeders and Expression Language sections.
Test application
In this guide, we will be implementing a script to load test the following application: https://ecomm.gatling.io. This is a sample e-commerce website where you can browse products, add to cart, checkout..etc. We encourage you to experiment with the platform to get familiar with its available actions. You may also open the network tab for further insights.
Identify relevant scenario(s)
The first step that we need to do before starting to write our script is identifying the relevant user journeys. Always remember that the end goal is to simulate real-world traffic, so taking the time to determine the typical user journeys on your application is crucial.
This can be done in several ways:
- Check your product analytics tool (e.g. Amplitude & Firebase)
- Check your APM tool (e.g. Dynatrace & Datadog)
- Asking the product-owner
For our e-commerce platform, we identified the following user journey:
- User lands on the homepage
- User logs in
- User lands again on the homepage (as an authenticated user)
- User adds a product to cart
- User buys (checkout)
Writing the script
Project structure
/*
.
├── .gatling/
└── src/
├── test/
├── java/
├── example/
├── endpoints/
├── APIendpoints.java
└── WebEndpoints.java
├── groups/
├── ScenarioGroups.java
├── utils/
├── Config.java
└── Keys.java
└── TargetEnvResolver.java
├── AdvancedSimulation.java
├── resources/
├── bodies/
├── data/
*/
/*
.
├── .gatling/
└── src/
├── endpoints/
├── apiEndpoints.js
└── webEndpoints.js
├── groups/
├── scenarioGroups.js
├── utils/
├── config.js
└── keys.js
└── targetEnvResolver.js
├── advancedSimulation.gatling.js
├── resources/
├── bodies/
├── data/
*/
/*
.
├── .gatling/
└── src/
├── gatling/
├── kotlin/
├── example/
├── endpoints/
├── APIendpoints.kt
└── WebEndpoints.kt
├── groups/
├── ScenarioGroups.kt
├── utils/
├── Config.kt
└── Keys.kt
└── TargetEnvResolver.kt
├── AdvancedSimulation.kt
├── resources/
├── bodies/
├── data/
*/
/*
.
├── .gatling/
└── src/
├── test/
├── scala/
├── example/
├── endpoints/
├── APIendpoints.scala
└── WebEndpoints.scala
├── groups/
├── ScenarioGroups.scala
├── utils/
├── Config.scala
└── Keys.scala
└── TargetEnvResolver.scala
├── AdvancedSimulation.scala
├── resources/
├── bodies/
└── data/
*/
Let’s break it down:
endpoints/
: Contains files responsible for defining individual requests, which we reference throughout our scenarios.- API endpoints file: Defines and manages API requests (backend calls) to our application.
- Web endpoints file: Defines and manages all frontend calls to our application.
groups/
: Contains files responsible for defining groups, which are collections of endpoints.- Scenario groups file: Defines and manages groups.
utils/
: Contains utility files designed to simplify and streamline processes.- Config file: Handles the retrieval of all necessary system properties and environment variables.
- Keys file: Contains predefined constants that serve as a single source of truth for virtual user session variable names. More on sessions here.
- Target env resolver: Responsible for resolving the targetEnv system property to the appropriate configuration.
resources/
: Contains feeder files and request bodies that we reference throughout our script.bodies/
: Contains the request bodies. For more information on referencing request bodies, see here.data/
: Contains the feeder files.
- Advanced simulation file: The main Gatling simulation file where we define the scenarios, http protocol, injection profile and assertions.
Endpoints
We need to define the individual requests that we call throughout the user journeys.
API Endpoints
We first define the API endpoints, i.e. the backend API calls and place them in a file under the /endpoints
directory.
Now let’s take a closer look at the following definition of the login
endpoint in the API endpoints file:
// Define login request
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#forms
public static final HttpRequestActionBuilder login = http("Login")
.post("/login")
.asFormUrlEncoded() // Short for header("Content-Type", "application/x-www-form-urlencoded")
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().is(200))
.check(jmesPath("accessToken").saveAs("AccessToken"));
// Define login request
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#forms
export const login = http("Login")
.post("/login")
.asFormUrlEncoded()
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().is(200))
.check(jmesPath("accessToken").saveAs("AccessToken"));
// Define login request
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#forms
val login: HttpRequestActionBuilder = http("Login")
.post("/login")
.asFormUrlEncoded()
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().`is`(200))
.check(jmesPath("accessToken").saveAs("AccessToken"))
// Define login request
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#forms
val login = http("Login")
.post("/login")
.asFormUrlEncoded
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status.is(200))
.check(jmesPath("accessToken").saveAs("AccessToken"))
- We use an http request action builder class to build a POST http request.
- We use
.asFormUrlEncoded()
to set the content-type header toapplication/x-www-form-urlencoded
. - We use
.formParam("username", "#{username}")
to set the form parameters of the POST request. More onformParam
here.- We use the Gatling Expression Language to retrieve the username’s value from the virtual user’s session. We will set this value later on in this guide using a Feeder.
- We use
.check()
for the following:
Web Endpoints
If the user journeys involve frontend calls that retrieve data (html, resources..etc) from the load-tested application server, then we need to define endpoints for these calls as well. Therefore we create another “web endpoints” file under the /endpoints
directory.
Now let’s take a look at the following definition of the homePage
endpoint:
// Define the home page request with response status validation
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#checks
public static final HttpRequestActionBuilder homePage = http("HomePage")
.get("https://ecomm.gatling.io")
.check(status().in(200, 304)); // Accept both OK (200) and Not Modified (304) statuses
// Define the home page request with response status validation
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#checks
export const homePage = http("HomePage")
.get("https://ecomm.gatling.io")
.check(status().in(200, 304)); // Accept both OK (200) and Not Modified (304) statuses
// Define the home page request with response status validation
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#checks
val homePage: HttpRequestActionBuilder = http("HomePage")
.get("https://ecomm.gatling.io")
.check(status().`in`(200, 304)) // Accept both OK (200) and Not Modified (304) statuses
// Define the home page request with response status validation
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#checks
val homePage = http("HomePage")
.get("https://ecomm.gatling.io")
.check(status.in(200, 304)) // Accept both OK (200) and Not Modified (304) statuses
- We define an http GET request to
https://ecomm.gatling.io
- We define a check to ensure we receive a response with status code corresponding to either 200 or 304.
Groups
Groups serve as a collection of multiple http requests, providing a clear logical separation for different parts of the user journey. Defining groups enables us to filter by them in the Gatling Enterprise reports, providing a more precise analysis of various processes within the load-tested application.
We define the groups in a file under the groups/
directory.
Let’s take a look at the following authenticate
group definition:
// Define a feeder for user data
// Reference: https://docs.gatling.io/reference/script/core/feeder/
private static final FeederBuilder<Object> usersFeeder = jsonFile("data/users_dev.json")
.circular();
// Define authentication process
public static final ChainBuilder authenticate = group("authenticate")
.on(loginPage, feed(usersFeeder), pause(5, 15), login);
// Define a feeder for user data
// Reference: https://docs.gatling.io/reference/script/core/feeder/
export const usersFeeder = jsonFile("data/users_dev.json").circular();
// Define authentication process
export const authenticate = group("authenticate")
.on(loginPage, feed(usersFeeder), pause(5, 15), login);
// Define a feeder for user data
// Reference: https://docs.gatling.io/reference/script/core/feeder/
private val usersFeeder = jsonFile("data/users_dev.json").circular()
// Define authentication process
val authenticate: ChainBuilder = group("authenticate")
.on(loginPage, feed(usersFeeder), pause(5, 15), login)
// Define a feeder for user data
// Reference: https://docs.gatling.io/reference/script/core/feeder/
private val usersFeeder = jsonFile("data/users_dev.json").circular
// Define authentication process
val authenticate: ChainBuilder = group("authenticate") (
loginPage, feed(usersFeeder), pause(5, 15), login)
-
We define a group under the name
authenticate
. -
This group will encompass the following two requests in the user journey: a
GET
request to retrieve the login page html and aPOST
request to the login endpoint. -
We use a feeder that injects dynamic data into our simulation. Here is how it works:
-
We first create a json file
users_dev.json
in the directory/resources/data
.[ { "username": "admin", "password": "gatling" }, { "username": "admin1", "password": "gatling1" } ]
-
We define
usersFeeder
that loads the json file usingjsonFile()
with thecircular()
strategy. More on feeder strategies here. -
We call the
feed(usersFeeder)
in theauthenticate
ChainBuilder to pass dynamicusername
andpassword
values to thelogin
endpoint that we defined earlier.
-
-
We also include a
pause(5, 15)
before the login step. This instructs the virtual user to pause for a random duration between 5 and 15 seconds. The randomness helps simulate human-like variations in navigation, such as filling out forms. Pauses are a crucial component of replicating real-world behavior, and it’s important to ensure they are placed appropriately throughout the scenario.
Scenarios
Now let’s define our scenarios! We will define two scenarios that showcase different user behaviors.
-
In our first scenario, we account for regional differences in user behavior commonly observed in e-commerce. To reflect this, we define two distinct user journeys based on the market: one for the French market and another for the US market:
// Define scenario 1 with a random traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#randomswitch static final ScenarioBuilder scn1 = scenario("Scenario 1") .exitBlockOnFail() .on( randomSwitch() .on( percent(70) .then( group("fr") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)), percent(30) .then( group("us") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)))) .exitHereIfFailed();
// Define scenario 1 with a random traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#randomswitch const scn1 = scenario("Scenario 1") .exitBlockOnFail() .on( randomSwitch().on( percent(70).then( group("fr").on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)), percent(30).then( group("us").on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)))) .exitHereIfFailed();
// Define scenario 1 with a random traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#randomswitch val scn1: ScenarioBuilder = scenario("Scenario 1") .exitBlockOnFail() .on( randomSwitch() .on( percent(70.0) .then( group("fr") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)), percent(30.0) .then( group("us") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy)))) .exitHereIfFailed()
// Define scenario 1 with a random traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#randomswitch private val scn1: ScenarioBuilder = scenario("Scenario 1") .exitBlockOnFail( randomSwitch( 70.0 -> group("fr")( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy), 30.0 -> group("us")( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy))) .exitHereIfFailed
Let’s take a closer look:
-
We wrap our scenario in an
exitBlockOnFail()
block to ensure that the virtual user exits the scenario whenever a request or check fails. This mimics real-world behavior, as users would be unable to proceed if they encounter blockers in the flow. Read more here. -
We use
randomSwitch()
to distribute traffic between two flows based on predefined percentages: 70% for the French (fr) market and 30% for the US (us) market. - TherandomSwitch()
will assign virtual users to the two flows according to the defined probabilities inpercent()
. -
Within each
percent()
block, we define the desired behavior. - More onrandomSwitch()
here.
-
-
In a similar manner, we define our second scenario:
// Define scenario 2 with a uniform traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#uniformrandomswitch static final ScenarioBuilder scn2 = scenario("Scenario 2") .exitBlockOnFail() .on( uniformRandomSwitch() .on( group("fr") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy), group("us") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy))) .exitHereIfFailed();
// Define scenario 2 with a uniform traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#uniformrandomswitch const scn2 = scenario("Scenario 2") .exitBlockOnFail() .on( uniformRandomSwitch().on( group("fr").on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy), group("us").on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy))) .exitHereIfFailed();
// Define scenario 2 with a uniform traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#uniformrandomswitch val scn2: ScenarioBuilder = scenario("Scenario 2") .exitBlockOnFail() .on( uniformRandomSwitch() .on( group("fr") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy), group("us") .on( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy))) .exitHereIfFailed()
// Define scenario 2 with a uniform traffic distribution // Reference: https://docs.gatling.io/reference/script/core/scenario/#uniformrandomswitch private val scn2: ScenarioBuilder = scenario("Scenario 2") .exitBlockOnFail( uniformRandomSwitch( group("fr")( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy), group("us")( homeAnonymous, pause(5, 15), authenticate, homeAuthenticated, pause(5, 15), addToCart, pause(5, 15), buy))) .exitHereIfFailed
HTTP protocol
Now, let’s define our http protocol builder.
// Define HTTP protocol configuration
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
static final HttpProtocolBuilder httpProtocol = http
.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36");
// Define HTTP protocol configuration
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
const httpProtocol = http
.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36");
// Define HTTP protocol configuration
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
val httpProtocol = http
.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36")
// Define HTTP protocol configuration
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
private val httpProtocol = http
.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36")
- We set the base url,
accept
anduser-agent
headers.
For the Authorization
header, we will have to set it per each API request that requires authentication, a bit of a headache no? To address this, we can use the following wrapper method:
// Add authentication header if an access token exists in the session
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#headers
public static final HttpProtocolBuilder withAuthenticationHeader(HttpProtocolBuilder protocolBuilder) {
return protocolBuilder.header(
"Authorization",
session -> Optional.ofNullable(session.getString("AccessToken")).orElse(""));
}
// Add authentication header if an access token exists in the session
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#headers
export function withAuthenticationHeader(protocolBuilder) {
return protocolBuilder.header(
"Authorization",
(session) => session.contains("AccessToken") ? session.get("AccessToken") : "");
}
// Add authentication header if an access token exists in the session
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#headers
fun withAuthenticationHeader(protocolBuilder: HttpProtocolBuilder): HttpProtocolBuilder {
return protocolBuilder.header("Authorization") { session ->
Optional.ofNullable(session.getString("AccessToken")).orElse("")
}
}
// Add authentication header if an access token exists in the session
// Reference: https://docs.gatling.io/reference/script/protocols/http/request/#headers
def withAuthenticationHeader(protocolBuilder: HttpProtocolBuilder): HttpProtocolBuilder = {
protocolBuilder.header(
"Authorization",
session => if (session.contains("AccessToken")) session("AccessToken").as[String] else "")
}
The method above takes an HttpProtocolBuilder
object and conditionally adds the Authorization
header to the requests:
- If the virtual user’s session contains the
AccessToken
key, setAuthorization
header to corresponding value. - Else, set
Authorization
to an empty string.
This will eliminate the need to set the Authorization
header individually for each request. Now we can define our http protocol builder like the following:
// Define HTTP protocol configuration with authentication header
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
static final HttpProtocolBuilder httpProtocolWithAuthentication = withAuthenticationHeader(
http.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"));
// Define HTTP protocol configuration with authentication header
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
const httpProtocolWithAuthentication = withAuthenticationHeader(
http.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"));
// Define HTTP protocol configuration with authentication header
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
private val httpProtocolWithAuthentication = withAuthenticationHeader(
http.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"));
// Define HTTP protocol configuration with authentication header
// Reference: https://docs.gatling.io/reference/script/protocols/http/protocol/
private val httpProtocolWithAuthentication = withAuthenticationHeader(
http.baseUrl("https://api-ecomm.gatling.io")
.acceptHeader("application/json")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"));
Injection profiles
We’ve defined our scenarios, i.e. the flows that the virtual user will go through. Now, we need to define how will these virtual users arrive into the load-tested application—the injection profile.
You should take the time to properly determine how the real-world users arrive to your application and accordingly, decide on the type of load that you will simulate on your application. For instance, are you looking to simulate load for a Black Friday? Are you looking to determine the maximum number of users your application can sustain? Deciding on what you are trying to simulate is necessary before defining the injection profile and starting your tests.
In our script, we define the following injection profiles according to the desired load test type:
- Capacity: Generally used to determine the maximum number of virtual users your application can sustain. Users arrival rate gets incremented over multiple levels and we analyze the metrics (response time, error ratio..etc) at each level according to our benchmarks.
- Soak: Generally used to monitor the application performance with a fixed load over a long period of time. Useful for checking memory leaks and database degradation over time.
- Stress: Generally used to push the application over its limits. We monitor how the system behaves under extreme load, does it recover properly from failures, does it autoscale as required?
- Breakpoint: Users arrival rate increases linearly. Useful in checking the “hard limit” at which the system starts to break. The key difference between a capacity test and a breakpoint test is that the latter typically reveals a “hard limit”, whereas a capacity test provides an estimate of the maximum number of users at which the system can maintain stable performance.
- Ramp-hold: Useful for simulating constant peak traffic. Users arrival rate increases up to a certain rate then stays at this rate for a period of time. Simulates real-world behavior of a Black Friday for example where the number of users stays at peak for a long period of time.
- Smoke: Test with one virtual user. Used to ensure that the scenario works and does not break.
- The injection profiles mentioned above are for open workload models, meaning the number of concurrent users is NOT capped (unlike some ticketing websites for example). For closed models or more information on open vs closed workload models, see here.
- Injection profiles can be defined according to your specific needs. The profiles provided are commonly used for the mentioned use cases, but they are not set in stone. Be sure to choose the injection profile that best fits your use case.
// Define different load injection profiles
// Reference: https://docs.gatling.io/reference/script/core/injection/
static final PopulationBuilder injectionProfile(ScenarioBuilder scn) {
return switch (testType) {
case "capacity" -> scn.injectOpen(
incrementUsersPerSec(1)
.times(4)
.eachLevelLasting(10)
.separatedByRampsLasting(4)
.startingFrom(10));
case "soak" -> scn.injectOpen(constantUsersPerSec(1).during(180));
case "stress" -> scn.injectOpen(stressPeakUsers(200).during(20));
case "breakpoint" -> scn.injectOpen(rampUsers(300).during(120));
case "ramp-hold" -> scn.injectOpen(
rampUsersPerSec(0).to(20).during(30),
constantUsersPerSec(20).during(60));
case "smoke" -> scn.injectOpen(atOnceUsers(1));
default -> scn.injectOpen(atOnceUsers(1));
};
}
// Define different load injection profiles
// Reference: https://docs.gatling.io/reference/script/core/injection/
const injectionProfile = (scn) => {
switch (testType) {
case "capacity":
return scn.injectOpen(
incrementUsersPerSec(1)
.times(4)
.eachLevelLasting({ amount: 10, unit: "seconds" })
.separatedByRampsLasting(4)
.startingFrom(10));
case "soak":
return scn.injectOpen(
constantUsersPerSec(1).during({ amount: 180, unit: "seconds" }));
case "stress":
return scn.injectOpen(stressPeakUsers(200).during({ amount: 20, unit: "seconds" }));
case "breakpoint":
return scn.injectOpen(
rampUsersPerSec(0).to(20).during({ amount: 120, unit: "seconds" }));
case "ramp-hold":
return scn.injectOpen(
rampUsersPerSec(0).to(20).during({ amount: 30, unit: "seconds" }),
constantUsersPerSec(20).during({ amount: 1, unit: "minutes" }));
case "smoke":
return scn.injectOpen(atOnceUsers(1));
default:
return scn.injectOpen(atOnceUsers(1));
}
};
// Define different load injection profiles
// Reference: https://docs.gatling.io/reference/script/core/injection/
private fun injectionProfile(scn: ScenarioBuilder): PopulationBuilder {
return when (testType) {
"capacity" -> scn.injectOpen(
incrementUsersPerSec(1.0)
.times(4)
.eachLevelLasting(10)
.separatedByRampsLasting(4)
.startingFrom(10.0))
"soak" -> scn.injectOpen(constantUsersPerSec(1.0).during(100))
"stress" -> scn.injectOpen(stressPeakUsers(200).during(20))
"breakpoint" -> scn.injectOpen(rampUsers(300).during(120))
"ramp-hold" -> scn.injectOpen(
rampUsersPerSec(0.0).to(20.0).during(30),
constantUsersPerSec(20.0).during(60))
"smoke" -> scn.injectOpen(atOnceUsers(1))
else -> scn.injectOpen(atOnceUsers(1))
}
}
// Define different load injection profiles
// Reference: https://docs.gatling.io/reference/script/core/injection/
private def injectionProfile(scn: ScenarioBuilder): PopulationBuilder = {
testType match {
case "capacity" => scn.inject(
incrementUsersPerSec(1)
.times(4)
.eachLevelLasting(10)
.separatedByRampsLasting(4)
.startingFrom(10))
case "soak" => scn.inject(constantUsersPerSec(1).during(180))
case "stress" => scn.inject(stressPeakUsers(200).during(20))
case "breakpoint" => scn.inject(rampUsers(300).during(120))
case "ramp-hold" => scn.inject(
rampUsersPerSec(0).to(20).during(30),
constantUsersPerSec(20).during(60))
case "smoke" => scn.inject(atOnceUsers(1))
case _ => scn.inject(atOnceUsers(1))
}
}
For more information on defining injection profiles using the Gatling DSL, refer to this section.
Define assertions
Now, we need to define assertions—the benchmarks that determine whether the test is considered successful or failed.
// Define assertions for different test types
// Reference: https://docs.gatling.io/reference/script/core/assertions/
static final List<Assertion> assertions = List.of(
global().responseTime().percentile(90.0).lt(500),
global().failedRequests().percent().lt(5.0));
static final List<Assertion> getAssertions() {
return switch (testType) {
case "capacity", "soak", "stress", "breakpoint", "ramp-hold" -> assertions;
case "smoke" -> List.of(global().failedRequests().count().lt(1L));
default -> assertions;
};
}
// Define assertions for different test types
// Reference: https://docs.gatling.io/reference/script/core/assertions/
const assertions = [
global().responseTime().percentile(90.0).lt(500),
global().failedRequests().percent().lt(5.0)
];
const getAssertions = () => {
switch (testType) {
case "capacity":
case "soak":
case "stress":
case "breakpoint":
case "ramp-hold":
return assertions;
case "smoke":
return [global().failedRequests().count().lt(1.0)];
default:
return assertions;
}
};
// Define assertions for different test types
// Reference: https://docs.gatling.io/reference/script/core/assertions/
private val assertions: List<Assertion> = listOf(
global().responseTime().percentile(90.0).lt(500),
global().failedRequests().percent().lt(5.0))
private fun getAssertions(): List<Assertion> {
return when (testType) {
"capacity", "soak", "stress", "breakpoint", "ramp-hold" -> assertions
"smoke" -> listOf(global().failedRequests().count().lt(1L))
else -> assertions
}
}
// Define assertions for different test types
// Reference: https://docs.gatling.io/reference/script/core/assertions/
private def assertions: Seq[Assertion] = Seq(
global.responseTime.percentile(90.0).lt(500),
global.failedRequests.percent.lt(5.0))
private def getAssertions(): Seq[Assertion] = {
testType match {
case "capacity" | "soak" | "stress" | "breakpoint" | "ramp-hold" => assertions
case "smoke" => Seq(global.failedRequests.count.lt(1L))
case _ => assertions
}
}
Add setUp block
Finally, we define the setup block. This configuration will execute both scenarios simultaneously. Based on the test type specified in the system properties, it will apply the corresponding injection profile and assertions.
// Set up the simulation with scenarios, load profiles, and assertions
{
setUp(injectionProfile(scn1), injectionProfile(scn2))
.assertions(getAssertions())
.protocols(httpProtocolWithAuthentication);
}
// Set up the simulation with scenarios, load profiles, and assertions
setUp(injectionProfile(scn1), injectionProfile(scn2))
.assertions(...getAssertions())
.protocols(httpProtocol);
// Set up the simulation with scenarios, load profiles, and assertions
init {
setUp(injectionProfile(scn1), injectionProfile(scn2))
.assertions(getAssertions())
.protocols(httpProtocolWithAuthentication)
}
// Set up the simulation with scenarios, load profiles, and assertions
setUp(injectionProfile(scn1),injectionProfile(scn2))
.assertions(getAssertions(): _*)
.protocols(httpProtocolWithAuthentication)
There also is the possibility to execute scenarios sequentially. For more information, please refer to this section.
Utility helpers
One last step would be adding some utility files to have a better organisation of the codebase. In the /utils
directory, we add the following files:
Configuration file
Responsible for defining Java System Properties/JavaScript parameters and Environment variables that we leverage in order to customize test behavior with no code changes. Let’s take a look at the following example:
public static final String testType = System.getProperty("testType", "smoke"); // Test type (default: smoke)
public static final String targetEnv = System.getProperty("targetEnv", "DEV");
export const testType = getParameter("testType", "stress");
export const targetEnv = getParameter("targetEnv", "DEV");
val testType: String = System.getProperty("testType", "smoke")
val targetEnv: String = System.getProperty("targetEnv", "DEV")
val testType: String = System.getProperty("type", "smoke")
val targetEnv: String = System.getProperty("targetEnv", "DEV")
- We define the
testType
system property that we use later on in the switch case of theinjectionProfile
method. - We define the
targetEnv
system property to specify the target application environment for the load simulation.
You may define additional Java system properties or environment variables as required to accommodate your scripting needs.
Keys file
Here, we define the session variable keys. This file centralizes your key references across all parts of your code in order to improve maintainability, avoiding typos, and keeping the code consistent and easier to refactor. Let’s take a look at the following key definition:
public static final String ACCESS_TOKEN = "AccessToken";
export const ACCESS_TOKEN = "AccessToken";
const val ACCESS_TOKEN = "AccessToken"
val ACCESS_TOKEN: String = "AccessToken";
Now for the login endpoint, instead of doing .saveAs("AccessToken")
here, we can do the following:
public static final HttpRequestActionBuilder login = http("Login")
.post("/login")
.asFormUrlEncoded() // Short for header("Content-Type", "application/x-www-form-urlencoded")
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().is(200))
.check(jmesPath("accessToken").saveAs(ACCESS_TOKEN));
const login = http("Login")
.post("/login")
.asFormUrlEncoded() // Short for header("Content-Type", "application/x-www-form-urlencoded")
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().is(200))
.check(jmesPath("accessToken").saveAs(ACCESS_TOKEN));
val login: HttpRequestActionBuilder = http("Login")
.post("/login")
.asFormUrlEncoded() // Short for header("Content-Type", "application/x-www-form-urlencoded")
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status().`is`(200))
.check(jmesPath("accessToken").saveAs(ACCESS_TOKEN))
val login = http("Login")
.post("/login")
.asFormUrlEncoded // Short for header("Content-Type", "application/x-www-form-urlencoded")
.formParam("username", "#{username}")
.formParam("password", "#{password}")
.check(status.is(200))
.check(jmesPath("accessToken").saveAs(ACCESS_TOKEN))
Target environment resolver file
You may need different configurations depending on the target application environment. Here, we define a method that takes the target environment as input and returns the corresponding configuration.:
pageUrl
: Frontend base URL.baseUrl
: Backend base URL.usersFeederFile
: The feeder file to use for user credentials.productsFeederFile
: The feeder file to use for products.
// Record to store environment-specific information
public record EnvInfo(
String pageUrl, String baseUrl, String usersFeederFile, String productsFeederFile) {}
// Resolve environment-specific configuration based on the target environment
public static EnvInfo resolveEnvironmentInfo(String targetEnv) {
return switch (targetEnv) {
case "DEV" -> new EnvInfo(
"https://ecomm.gatling.io",
"https://api-ecomm.gatling.io",
"data/users_dev.json",
"data/products_dev.csv");
default -> new EnvInfo(
"https://ecomm.gatling.io",
"https://api-ecomm.gatling.io",
"data/users_dev.json",
"data/products_dev.csv");
};
}
// Resolve environment-specific configuration based on the target environment
const targetEnvResolver = (targetEnv) => {
switch (targetEnv) {
case "DEV":
return {
pageUrl: "https://ecomm.gatling.io",
baseUrl: "https://api-ecomm.gatling.io",
usersFeederFile: "data/users_dev.json",
productsFeederFile: "data/products_dev.csv"
};
default:
return {
pageUrl: "https://ecomm.gatling.io",
baseUrl: "https://api-ecomm.gatling.io",
usersFeederFile: "data/users_dev.json",
productsFeederFile: "data/products_dev.csv"
};
}
};
// Data class to store environment-specific information
data class EnvInfo(
val pageUrl: String, val baseUrl: String, val usersFeederFile: String, val productsFeederFile: String)
// Object to resolve environment-specific configuration based on the target environment
object TargetEnvResolver {
// Resolve environment-specific configuration based on the target environment
fun resolveEnvironmentInfo(targetEnv: String): EnvInfo {
return when (targetEnv) {
"DEV" -> EnvInfo(
pageUrl = "https://ecomm.gatling.io",
baseUrl = "https://api-ecomm.gatling.io",
usersFeederFile = "data/users_dev.json",
productsFeederFile = "data/products_dev.csv")
else -> EnvInfo(
pageUrl = "https://ecomm.gatling.io",
baseUrl = "https://api-ecomm.gatling.io",
usersFeederFile = "data/users_dev.json",
productsFeederFile = "data/products_dev.csv")
}
}
}
// Record to store environment-specific information
case class EnvInfo(
pageUrl: String, baseUrl: String, usersFeederFile: String, productsFeederFile: String)
// Resolve environment-specific configuration based on the target environment
def resolveEnvironmentInfo(targetEnv: String): EnvInfo = targetEnv match {
case "DEV" => EnvInfo(
"https://ecomm.gatling.io",
"https://api-ecomm.gatling.io",
"data/users_dev.json",
"data/products_dev.csv")
case _ => EnvInfo(
"https://ecomm.gatling.io",
"https://api-ecomm.gatling.io",
"data/users_dev.json",
"data/products_dev.csv")
}