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.

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:

  1. User lands on the homepage
  2. User logs in
  3. User lands again on the homepage (as an authenticated user)
  4. User adds a product to cart
  5. 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"))

  1. We use an http request action builder class to build a POST http request.
  2. We use .asFormUrlEncoded()to set the content-type header to application/x-www-form-urlencoded.
  3. We use .formParam("username", "#{username}") to set the form parameters of the POST request. More on formParam 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.
  4. We use .check() for the following:
    • Validate that we receive a 200 status code in the response. More on validating here.
    • Extract the accessToken from the response body and save it to the user session under the name AccessToken. Further information on extracting can be found here, and on saving here.
    • More on jmesPath here.

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
  1. We define an http GET request to https://ecomm.gatling.io
  2. 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)
  1. We define a group under the name authenticate.

  2. This group will encompass the following two requests in the user journey: a GET request to retrieve the login page html and a POST request to the login endpoint.

  3. 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 using jsonFile() with the circular() strategy. More on feeder strategies here.

    • We call the feed(usersFeeder) in the authenticate ChainBuilder to pass dynamic username and password values to the login endpoint that we defined earlier.

  4. 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. - The randomSwitch() will assign virtual users to the two flows according to the defined probabilities in percent().

    • Within each percent() block, we define the desired behavior. - More on randomSwitch() 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 and user-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, set Authorization 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.

       
// 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 the injectionProfile 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")
}

Edit this page on GitHub