Gatling : Load Test As Code

Narasimhan V
5 min readNov 29, 2024

--

I have done some performance improvements on my API. I would like to do load tests to validate. However, the performance engineering team’s calendar is booked for the next 2 weeks. Does it sound very familiar?

Gatling is a “load test as code” tool. Developers can write load tests using their IDE like how they write unit tests. Gatling has documentation for Java, Javascript, Scala & Kotlin. However, the community support is more for Scala and comparatively less for Java. The motivation behind this article is to develop a flow for load test thereby covering some details around how to use in Java some of the key concepts of Gatling.

The use case covered in this article is simple. A “create user” api is called followed by “add to cart” for the user. The entire user information is provided as input along with items to add into the cart. The key Gatling concepts covered as part of this flow are:

  • Setup Execution of API calls
  • Using feeders to inject user and item data from json files into executions
  • Using session variables to pass data between executions
  • Verifying execution responses using checks
  • Set up of scenario that chains executions
  • Setup simulations to run the scenario with http protocol and user ramp up

Gradle dependencies for Gatling

plugins {
id 'io.gatling.gradle' version '3.10.3'
}

dependencies {
testImplementation 'io.gatling:gatling-test-framework:3.10.3'
testImplementation 'io.gatling.highcharts:gatling-charts-highcharts:3.10.3'
}

The following code shows execution setup for creating a user

private FeederBuilder.FileBased<Object> userJsonFileFeeder(){
return jsonFile("json/user.json").circular();
}

private String getRequestBodyForUser() {
String user = "{\"emailAddress\" : \"#{emailAddress}\"," +
"\"firstName\" : \"#{firstName}\"," +
"\"lastName\" : \"#{lastName}\"," +
"\"phoneNumber\" : \"#{phoneNumber}\"}";
return user;
}

public ChainBuilder createUser() {
return exec(
http("create_user")
.post("/ecommerce/user")
.body(StringBody(getRequestBodyForUser()))
.asJson()
.check(status().is(200), jsonPath("$").saveAs("user"))
);
}
  • The user json file is used by a feeder (in the method userJsonFileFeeder()). The feeder injects the user data from the json file in a sequential fashion during the api execution.
  • The injected data is used to create the request body for the api calls. The getRequestBodyForUser() method creates a json string for the payload.
  • The feeder injected data are obtained from session variables #{emailAddress}, #{firstName}, #{lastName}, “{phoneNumber} to create the json string.
  • The execution makes the api call. Upon successful execution (status 200), the entire response of the api (jsonPath “$”) is stored in the session variable “user”.

A similar execution setup is done for cart as below:

  • The feeder injects the items data from the json file into the virtual user session.
  • The add_to_cart execution obtains the data from session for itemNo, quantity and also user (remember the user response from “create user” was set in session) to create the payload for the addToCart api.
private FeederBuilder.FileBased<Object> cartJsonFileFeeder(){
return jsonFile("json/items.json").circular();
}

private String getRequestBodyForCart(String user, String itemNo, String quantity) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
UserDto userDto = mapper.readValue(user, UserDto.class);
CartDto cartDto = CartDto.builder()
.itemNo(itemNo)
.user(userDto)
.quantity(quantity)
.build();
return mapper.writeValueAsString(cartDto);
}

public ChainBuilder addToCart(){
return exec(
http("add_to_cart")
.post("/ecommerce/cart")
.body(StringBody(session -> {
try {
return getRequestBodyForCart(session.get("user"), session.get("itemNo"), session.get("quantity"));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
))
.asJson()
.check(status().is(200))
);
}

A scenario builder chains the above two execution steps to create a scenario. It can be seen from the code that the feeders are used to inject data into the executions.

private ScenarioBuilder buildcreateUserAndCartScenario() throws JsonProcessingException {
ScenarioBuilder createUserAndCartScenario = scenario("Create User Perf Test")
.feed(userJsonFileFeeder())
.exec(createUser())
.feed(cartJsonFileFeeder())
.exec(addToCart());

return createUserAndCartScenario;
}

A simulation test can be run using the above scenario along with defining the http protocols and also the number of users to be used for the test execution

private HttpProtocolBuilder buildLocalProtocol(){
HttpProtocolBuilder localServer = http
.baseUrl("http://localhost:8080") // Remote urls can be used as well
.contentTypeHeader("application/json");
return localServer;
}

private OpenInjectionStep setUpRampUsersPerSec(){
return rampUsersPerSec(1).to(50).during(10); // Gradually increase from 1 to 50 during 10 secs
}

public CartSimulationTest() throws JsonProcessingException {
setUp(
buildcreateUserAndCartScenario()
.injectOpen(setUpRampUsersPerSec())
.protocols(buildLocalProtocol())
);
}

The CartSimulationTest can be run from the commandline as

gradlew gatlingRun-simulations.CartSimulationTest

A performance test execution report provides visual metrics like

  • Execution errors
  • Active users during the simulation
  • Response time percentiles

So far, we could see that both create user and add to cart were executed multiple times. However, there could be situations where all api calls need not be executed with the same workload. For example, an authorization token needs to be generated and passed in the header when making api calls. However, this token needs to be generated only once for the entire workload execution. Gatling allows to achieve this by chaining multiple scenarios using “andThen”.

private OpenInjectionStep setUpConstantUsersPerSec(){
return constantUsersPerSec(1).during(1);
}

public CartSimulationTest() throws JsonProcessingException {
setUp(
buildGetTokenScenario()
.injectOpen(setUpConstantUsersPerSec())
.andThen(
buildcreateUserAndCartScenario()
.injectOpen(setUpRampUsersPerSec())
.protocols(buildLocalProtocol())
)
);
}

Here the token scenario is executed only once (as the user setup is done for 1 user with 1 execution. However, once the token is generated it cannot be passed between scenarios. This is because Gatling executes the different scenarios using different virtual users with their own separate session contexts. So, if parameters have to be passed across scenarios then it needs to be achieved using class level variables or someother persistent storage.

Final Thoughts

Gatling does have some learning curve. And a comparatively lesser community support for Java may be a deterrent. However for developers who can learn, it provides a quick way to develop load test simulations and run proactively early in the development life cycle without waiting for performance engineering team availability.

--

--

No responses yet