In this post, I am going to show how to create load tests for a REST API application with the help of Gatling 2. This will be a step-by-step guide — starting from integrating Gatling 2 using SBT plugin, creating/configuring test scenarios, and all the way to running the Gatling tests.
Gatling 2 stress tool sends simultaneous requests following a specific scenario. The scenario consists of requests to the service and response checks, so we can emulate the service usage under a high load. At the end of a simulation, Gatling 2 provides users with a detailed HTML report (you can find one here). Using Gatling 2 will help to find out how reliable a service is and how well it performs. Also, it may help you to find and fix various performance issues before you encounter significant problems in production.
For the sake of demonstration, I am going to create a simulation for the service built in the “Building the REST service with Scala” article. You can see the service sources here. The Gatling SBT plugin will be used to run the tests with just one SBT command.
There are also other extensions which can be used to run the simulation, see the full list here.
We will create the tests in 4 steps:
- Create SBT project and configure dependencies
- Specify the SBT project configuration
- Generate test data for the load test
- Create a test scenario
Step 1. Create SBT project and configure dependencies
First, create the SBT project and specify the SBT version:
project/build.properties
sbt.version=0.13.5
Gatling 2 uses Akka actors, so we will add this dependency to build.sbt
, also we will add akka-slf4j package for logging, and lift-json for JSON serialization:build.sbt
import io.gatling.sbt.GatlingPlugin
name := "rest"
version := "1.0"
scalaVersion := "2.10.4"
val akkaVersion = "2.2.4"
libraryDependencies ++= Seq(
"com.typesafe.akka" % "akka-actor_2.10" % akkaVersion,
"com.typesafe.akka" % "akka-slf4j_2.10" % akkaVersion,
"net.liftweb" % "lift-json_2.10" % "2.5.1"
)
resolvers ++= Seq(
"Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
)
scalacOptions ++= Seq("-Xmax-classfile-name", "100")
Code language: JavaScript (javascript)
Then, following the Gatling SBT plugin setup instructions:
- add the Gatling SBT plugin in
plugins.sbt
resolvers ++= Seq(
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
)
addSbtPlugin("io.gatling" % "sbt-plugin" % "1.0-RC5")
Code language: JavaScript (javascript)
- import Gatling 2 and configure settings in the
*.scala
build file
project/TestBuild.scala
import sbt._
import sbt.Keys._
import io.gatling.sbt.GatlingPlugin._
object TestBuild extends Build {
val libs = Seq(
"io.gatling.highcharts" % "gatling-charts-highcharts" % "2.0.0-SNAPSHOT" % "test",
"io.gatling" % "gatling-bundle" % "2.0.0-SNAPSHOT" % "test"
artifacts Artifact("gatling-bundle", "zip", "zip", "bundle"),
"io.gatling" % "test-framework" % "1.0-SNAPSHOT" % "test"
)
val root = Project("scala-rest", file("."))
.settings(gatlingSettings: _*)
.settings(resolvers +=
"Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
.configs(Gatling)
.settings(organization := "io.gatling.sbt.test")
.settings(libraryDependencies ++= libs)
.settings(scalaSource in Gatling :=
new File(s"${System.getProperty("user.dir")}/src/test/gatling/scala"))
}
Code language: JavaScript (javascript)
The scalaSource in Gatling
determines the folder of the Gatling test sources. And the System.getProperty("user.dir")
is used here to get a full path to the project root directory.
Step 2. Specify the SBT project configuration
Place the following configuration into the application.conf
file
src/test/resources/application.conf
service {
# Url to service host:
host = "http://localhost:8080"
# Endpoints base path:
api_link = "/customer"
}
scenario {
# Scenario repeat count:
repeat_count = 1
# Emulate the specific count of users for simulation:
thread_count = 2
# Percent of successful service responses
# when the simulation is considered to be successful:
percent_success = 100
}
# Test data:
data {
first_names = ["Andrey", "Vlad", "Alexandra"]
last_names = ["Litvinenko", "Belik", "Borte"]
birthdays = ["1956-01-24", "1995-02-28", "1983-12-16"]
}
Code language: PHP (php)
Let’s create a trait which will help us to get the configuration data from the application.conf
file
src/test/gatling/com/sysgears/example/rest/SimulationConfig.scala
import akka.event.slf4j.SLF4JLogging
import com.typesafe.config.{ConfigException, ConfigFactory}
import scala.util.Try
trait SimulationConfig extends SLF4JLogging {
/**
* Application config object.
*/
private[this] val config = ConfigFactory.load()
/**
* Gets the required string from the config file or throws
* an exception if the string is not found.
*
* @param path path to string
* @return string fetched by path
*/
def getRequiredString(path: String) = {
Try(config.getString(path)).getOrElse {
handleError(path)
}
}
/**
* Gets the required int from the config file or throws
* an exception if the int is not found.
*
* @param path path to int
* @return int fetched by path
*/
def getRequiredInt(path: String) = {
Try(config.getInt(path)).getOrElse {
handleError(path)
}
}
/**
* Gets the required string list from the config file or throws
* an exception if the string list is not found.
*
* @param path path to string list
* @return string list fetched by path
*/
def getRequiredStringList(path: String) = {
Try(config.getStringList(path)).getOrElse {
handleError(path)
}
}
private[this] def handleError(path: String) = {
val errMsg = s"Missing required configuration entry: $path"
log.error(errMsg)
throw new ConfigException.Missing(errMsg)
}
/**
* URL for test.
*/
val baseURL = getRequiredString("service.host")
/**
* Endpoint link.
*/
val customerLink = getRequiredString("service.api_link")
/**
* Scenario repeat count.
*/
val repeatCount = getRequiredInt("scenario.repeat_count")
/**
* Count of users for simulation.
*/
val threads = getRequiredInt("scenario.repeat_count")
/**
* Percent of successful service responses when
* the simulation is considered to be successful.
*/
val percentSuccess = Try(config.getInt("scenario.percent_success")).getOrElse(100)
}
Code language: PHP (php)
Step 3. Generate test data for the load test
Let’s add the Customer
case class as a container for test data. The class should comply to the Customer data of the tested service:
src/test/gatling/com/sysgears/example/rest/data/Customer.scala
import java.util.Date
/**
* Customer entity.
*
* @param id unique id
* @param firstName first name
* @param lastName last name
* @param birthday date of birth
*/
case class Customer(id: Option[Long],
firstName: String,
lastName: String,
birthday: Option[Date])
Code language: CSS (css)
In order to separate test data configuration from the test scenario, let’s add the base object for the class containing the test scenario:
src/test/gatling/com/sysgears/example/rest/data/CustomerTestData.scala
import java.text.SimpleDateFormat
import com.sysgears.example.rest.SimulationConfig
import scala.util.Random
object CustomerTestData extends SimulationConfig {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd")
//test data
val firstNames = getRequiredStringList("data.first_names")
val lastNames = getRequiredStringList("data.last_names")
val birthdays = getRequiredStringList("data.birthdays")
def generateCustomer: Customer = {
Customer(None,
firstNames.get(Random.nextInt(firstNames.size())),
lastNames.get(Random.nextInt(lastNames.size())),
Some(dateFormat.parse(birthdays.get(Random.nextInt(birthdays.size()))))
)
}
val customers = List.fill(threads)(generateCustomer)
val nonExistentCustomerId = customers.size + 1
}
Code language: JavaScript (javascript)
Step 4. Create a test scenario
Gatling simulation follows a specific template:
- importing of
io.gatling.core.Predef._
andio.gatling.http.Predef._
- extending
io.gatling.core.scenario.Simulation
Gatling API base class - providing the actual scenario (requests to a tested service and response checks)
- setting up the scenario settings
First, let’s look at the sample class, which will help you understand the basic principles of creating the Gatling scenario class:
// essential imports for simulation:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
// simulation class:
class CustomerSimulation extends io.gatling.core.scenario.Simulation {
//scenario
val scn = scenario("Simulation name").repeat(repeatCount) {
exec(
http(session => "Request description")
.post(fullEndpointLink) // full link to tested endpoint
.check(status is rightStatus) // successful status
)
}
// set up the scenario and threads (users) count:
setUp(scn.inject(atOnceUsers(usersCount)))
}
Code language: JavaScript (javascript)
Now let’s create a simulation for the tested service, this simulation will:
- Send CRUD requests to the service and check the response.
- Configure an http base URL of the service.
- Use JSONPath for processing JSON in responses.
- Configure the scenario to use the specific number of simultaneous requests.
- Configure the scenario to repeat it as many times as it was set in SimulationConfig.
Here is a full simulation code:
src/test/gatling/com/sysgears/example/rest/CustomerSimulation.scala
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import net.liftweb.json.Serialization
import com.sysgears.example.rest.data.CustomerTestData._
import scala.util.Random
/**
* Load test for the rest service.
*/
class CustomerSimulation extends io.gatling.core.scenario.Simulation
with SimulationConfig {
/**
* http configuration.
*/
val httpProtocol = http.baseURL(baseURL)
/**
* Set default formats for json parser.
*/
implicit val formats = net.liftweb.json.DefaultFormats
/**
* Returns a random customer in JSON format.
*
* @return customer in JSON format
*/
def randCustomer = {
StringBody(Serialization.write(
customers(Random.nextInt(customers.size))))
}
/**
* Generates a random search query with random params.
*
* @return generated query
*/
def randSearchQuery = {
def ?+(s: String) = {
if (Random.nextBoolean()) s + "&" else ""
}
val customer = customers(Random.nextInt(customers.size))
"?" + ?+(s"firstName=${customer.firstName}") +
?+(s"lastName=${customer.lastName}") +
?+(s"birthday=${dateFormat.format(customer.birthday.get)}")
}
/**
* Scenario for simulation.
*/
val scn = scenario("Simulation for the customer service").repeat(repeatCount) {
exec(
http(session => "Post a customer")
.post(customerLink)
.body(randCustomer)
.check(status is 201)
.check(jsonPath("$.id").saveAs("id"))
)
.exec(
http(session => "Search customers")
.get(customerLink + randSearchQuery)
.check(status is 200)
)
.exec(
http(session => "Get customers")
.get(customerLink)
.check(status is 200)
)
.exec(
http(session => "Get customer by id")
.get("/customer/${id}")
.check(status is 200)
)
.exec(
http(session => "Put customer by id")
.put("/customer/${id}")
.body(randCustomer)
.check(status is 200)
)
.exec(
http(session => "Delete customer")
.delete(customerLink + "/${id}")
.check(status is 200)
)
}
/**
* Sets the scenario.
*/
setUp(scn.inject(atOnceUsers(threads)))
.protocols(httpProtocol)
.assertions(global.successfulRequests.percent.is(percentSuccess)) //Check test result
}
Code language: JavaScript (javascript)
We need to start the service before running the load tests. You can find the service jar with the config file in the tested_service
directory of the repository, make sure to set up proper mysql parameters in the application.conf
file in order for application to access your database server and don’t forget to create an appropriate MySQL database. Now you can start the jar using the following command:
$ java -Dconfig.file=application.conf -jar rest-assembly-1.0.jar
And run Gatling simulations with the following command:
$ sbt gatling:test
After the simulation is finished, you can find the report in the target/gatling/{simulation-name}
directory.
All the sources are available on GitHub repository.
See also:
- Gatling documentation.
- Scala REST API Integration Testing with Spray-testkit
Hope you find this helpful.