Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

Gatling is a load-testing framework mainly used to test REST APIs. It’s designed to be easy to use and maintain. This is why the testing scenarios can be written using code and configuration files. It’s a free tool, but it also offers an enterprise edition.

Gatling is an open-source tool written in Scala, but lately, it also supports Java sources to make it easier for the Java community to use. The API is almost the same between Java and Scala, so it’s easy to use if we’re familiar with any of those languages. But because Gatling was originally shipped with Scala, we might run into the need to maintain or extend Scala implementations more often.

In this tutorial, we’ll explore Gatling using Scala. We’ll see how to easily write scenarios to performance test our REST application, no matter the application’s implementation language. Also, the basic concepts of Gatling are discussed, so we won’t go into much detail about them.

2. Create Requests in Gatling Using Scala

In order to test a REST API, we need to define the REST requests in Gatling. But first, we need to implement the endpoint against which we want to run performance tests.

2.1. The REST Service API

Testing different REST operations in Gatling is done in a pretty similar way. So, for our examples, we’re going to use a REST API with a simple GET request, but POST, DELETE, etc. should be similar.

We use Scala Play, but any framework and any language can be used instead. We’ll be testing the TodoListController, which has an endpoint todo(), returning a simple 200 response. The path to reach this endpoint is /todo.

2.2. The Galting Request in Scala

On the Gatling side, we need to use HttpRequestBuilder to create the HTTP request to test this endpoint:

val request: HttpRequestBuilder = http(requestName)
  .get(requestPath)
  .check(status.is(expectedResponseStatus))
  .check(bodyString.optional.saveAs("sBodyString"))

The minimum parameters needed for this GET request in Gatling using Scala are:

  • the request name, to describe the request, like request_todo_endpoint
  • the request path, which should be the path of the specific request, like /todo
  • the checks, where we can run some checks to validate the response or save some response fields in variables for other usages

In our solution, we’ve added the response body in the sBodyString variable to be able to log it in error cases. In this case, we log some information to make debugging easier.

2.3. Enrich the Request With Debugging Information Using Session

In order to be able to better debug issues, we can use the Session object to wrap the HttpRequestBuilder. This way we can check the status of a request and if there are failures, we can add some logs and more:

object ChainRequestsProvider {
    def simpleRequest(requestName: String, requestPath: String, expectedResponseStatus: Int): ChainBuilder = {
        val request: HttpRequestBuilder = http(requestName)
          .get(requestPath)
          .check(status.is(expectedResponseStatus))
          .check(bodyString.optional.saveAs("sBodyString"))

        exec(session => session.markAsSucceeded)
          .exec(request).doIf(_.isFailed) {
              exec { session =>
                  println("***Failure on [" + requestPath + "] endpoint:")
                  print("Gatling Session Data: ")
                  println(session.attributes.get("sBodyString"))
                  session
              }
          }
    }
}

At the beginning of each request, we set the session as successful. This is because, in Gatling using Scala, the session maintains its status even after some different requests. So we reset the session at the beginning of each request. Then we check if it’s failed, to validate if the current request check failed. Finally, we log the data we need for debugging, if we get a failure on the request.

Notably, we don’t execute the request yet. We’ve only created a ChainBuilder object that we’ll use later. We’re still missing some configurations in order to execute the request, like the base URL, the test load, etc. Moreover, this object is only a part of the broader test simulation.

3. Create Scenarios in Gatling Using Scala

As mentioned earlier, the HttpRequestBuilder or ChainBuilder object we create to test a specific endpoint is still missing some configuration. We need to set a base URL and the test load configuration. To do this in Gatling using Scala, we use the scenario() method:

object ScenariosProvider {
    private val httpProtocol = http.baseUrl("http://localhost:9000").disableCaching.disableFollowRedirect

    def getScenario(scenarioName: String, request: ChainBuilder, tps: Double, rampUpSeconds: Integer, durationSeconds: Integer): PopulationBuilder = {
        scenario(scenarioName)
          .exec(request)
          .inject(
              rampUsersPerSec(0).to(tps).during(rampUpSeconds),
              constantUsersPerSec(tps).during(durationSeconds-rampUpSeconds-rampUpSeconds),
              rampUsersPerSec(tps).to(0).during(rampUpSeconds)
          )
          .protocols(httpProtocol)
    }
}

The scenario() method expects a minimum configuration of:

  • the scenario name, which is a name to recognize this specific scenario, like request_todo_endpoint
  • the request to execute, that is some request builder, like HttpRequestBuilder or ChainBuilder
  • the inject parameters, which are the injected users numbers per time unit
  • the protocol is the configuration for the HTTP scheme, like https, the base URL, caching options, etc

Regarding the injected users, Gatling in Scala provides multiple ways to set the load. The load, in transactions per second (tps) can be set in a constant manner or by specifying ramp periods to increase or decrease the load steadily. We also have to set the period of the tps and finally combine different injectors.

In this scenario, we set a ramp-up period to get the users from 0 to some value in a specific period of time, then keep the load steady for some duration, and in the final configured time ramp down to 0.

The returned object is the PopulationBuilder. We’ll use it in the next step, when we create the simulation, along with other PopulationBuilder objects for other endpoints or configurations.

4. Create and Execute Simulations in Gatling Using Scala

Bringing everything together, for load tests in Gatling using Scala, we need to define a Simulation with all of the different scenarios we want to test in parallel. The simulation can include testing different endpoints, testing with different loads, testing failing scenarios, and more:

class PeakLoadSimulation extends Simulation {
    setUp(
        getScenario("getExistingEndpoint", simpleRequest("request_todo_endpoint", "/todo", 200), 50, 10, 60),
        getScenario("nonExistingEndpoint", simpleRequest("request_wrong_endpoint", "/not-todo", 200), 5, 10, 60)
    )
}

The simulation we create, PeakLoadSimulation, needs to extend the Simulation class. Then, in the setUp() method, we define the scenarios as PopulationBuilder objects. We can add as many scenarios as we want. All of the different scenarios are to be executed in parallel.

When executing the simulation against the REST service API we defined earlier, we should get a successful execution:

simulation outcome

Here, we can see the generated report that contains the request count, response times, and the XXth percentile of response times. The request count validates that the requests we made to the existing endpoint are ten times more than the ones we did against the non-existing one.

5. Add Assertions in Gatling Using Scala

When running the simulations, we can monitor the outcome in different ways. We could be testing if the server will handle the load off or if it will start throwing OutOfMemory errors. We could also be getting info out of dashboards from the metrics the service exposes.

Gatling using Scala provides ways to validate the execution outcomes from the testing framework side, using assertions against the scenarios. Those assertions can be per scenario and test different metrics, like the successful response rate, the response time, etc:

class PeakLoadSimulation extends Simulation {
    setUp(
        getScenario("getExistingEndpoint", simpleRequest("request_todo_endpoint", "/todo", 200), 50, 10, 60),
        getScenario("nonExistingEndpoint", simpleRequest("request_wrong_endpoint", "/not-todo", 200), 5, 10, 60)
    ).assertions(
        details("request_todo_endpoint").successfulRequests.percent.gt(99.99),
        details("request_todo_endpoint").responseTime.percentile4.lt(20),
        details("request_todo_endpoint").requestsPerSec.gt(40),
        details("request_wrong_endpoint").successfulRequests.percent.lt(1),
        details("request_wrong_endpoint").responseTime.percentile4.lt(20)
    )
}

We enrich our simulation by adding four assertions for the two scenarios. Each scenario checks at the end of the execution that the successful rate is the expected one and the response time of 99% of the responses (percentile4) is less than 20 milliseconds, which makes sense when testing servers on localhost. Last, we add the requestsPerSec, to verify that the load we want per scenario is the expected one.

The successfulRequests field should accept as successful responses the ones that pass the status check we set in the simpleRequest method. For example, if we change the missing endpoint scenario to expect 404, simpleRequest(“request_wrong_endpoint”, “/not-todo”, 404), then the successfulRequests percent should also be changed to validate that is greater than 99.

Notably, the value in the details() method matches the name we give to the HttpRequestBuilder object as requestName.

When we run the updated simulation, with the assertions, we should again get a successful execution:

gatling simulation with assertions outcome

This time, we see a similar report, but the outcome of the assertions is also included. First goes the requestName, then goes the description of the assertion in plain text, and last comes the outcome, true or false, followed by some extra info.

6. Conclusion

In this article, we went through Gatling using Scala, by discussing all the components needed to create a load test scenario against a REST endpoint. We looked at the basic configuration needed to create simple tests. To make the concepts clear, we demonstrated each concept with a Simulation that tests a couple of endpoints.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.