Creating a Gatling Simulation
Background #
I could not find material online that targeted the latest version of Gatling, meant for someone without a great deal of functional experience. I also don't mean for this to be comprehensive; I just hope this serves as a starting point to jump off to official documentation.
Recommended Reading #
What is functional programming
What is Gatling #
Gatling is a load testing framework written in Scala. It efficiently simulates HTTP traffic in a way which allows thousands of concurrent users from a single machine. Gatling does not parse HTML or run Javascript; if this is your objective you will need to look elsewhere for that. However, by not rendering HTML, loading dependencies, or running Javascript, Gatling scales incredibly well.
There are two ways to begin writing a simulation: use the recorder for a basic starting simulation, or manually write one from scratch. As I did not have the luxury of starting with a recorder, I cannot discuss how to begin that way. I would imagine, however, that even if you record a simulation, you will still be editing it. So, most of this post will be covering how to manually write a Gatling simulation. I've provided a sample Gatling project to help follow along here.
Recording a Simulation #
Gatling supports recording simulations using its HTTP recorder. It acts as a proxy which records all traffic between a browser and a server and then saves it into a simulation. If the target website is not https or a custom, self-signed certificate can be used, this method of creating simulations is ideal. For the website I started load testing, it wasn't possible to use this recorder, so I am unaware of most of its functionality.
Manually Creating a Simulation #
Structuring a simulation is defined at length here, but a minimal functioning example helps illustrate this:
package simpleexample
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class ExampleSimulation extends Simulation {
val httpConf = http
.baseURL("https://google.com")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.doNotTrackHeader("1")
.acceptLanguageHeader("en-US,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")
val scn = scenario("Scenario Name")
.exec(http("Main Page")
.get("/"))
setUp(scn.inject(atOnceUsers(1)).protocols(httpConf))
}
This simulation loads https://google.com/ with the provided HTTP headers with a single user.
The Simulation above describes a single scenario, although there can be many. This scenario
has a single HTTP chain "Main Page"; it visits /
relative to the baseURL described in
our httpConf
.
The setUp
function called at the end of the class definition is what defines the simulation's execution
parameters. In this case, a single user immediately begins the scenario, walking through the single
step of getting /
. There are many other options for defining
how user(s) enter the simulation and
at what rate they enter the simulation.
The output of the example simulation above is the following:
Simulation simpleexample.ExampleSimulation started...
================================================================================
2017-03-15 12:47:51 2s elapsed
---- Requests ------------------------------------------------------------------
> Global (OK=2 KO=0 )
> Main Page (OK=1 KO=0 )
> Main Page Redirect 1 (OK=1 KO=0 )
---- Scenario Name -------------------------------------------------------------
[##########################################################################]100%
waiting: 0 / active: 0 / done:1
================================================================================
Simulation simpleexample.ExampleSimulation completed in 0 seconds
Parsing log file(s)...
Parsing log file(s) done
Generating reports...
================================================================================
---- Global Information --------------------------------------------------------
> request count 2 (OK=2 KO=0 )
> min response time 159 (OK=159 KO=- )
> max response time 477 (OK=477 KO=- )
> mean response time 318 (OK=318 KO=- )
> std deviation 159 (OK=159 KO=- )
> response time 50th percentile 318 (OK=318 KO=- )
> response time 75th percentile 398 (OK=398 KO=- )
> response time 95th percentile 461 (OK=461 KO=- )
> response time 99th percentile 474 (OK=474 KO=- )
> mean requests/sec 2 (OK=2 KO=- )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 2 (100%)
> 800 ms < t < 1200 ms 0 ( 0%)
> t > 1200 ms 0 ( 0%)
> failed 0 ( 0%)
================================================================================
Reports generated in 0s.
Please open the following file: target/gatling/examplesimulation-1489600069122/index.html
[info] Simulation ExampleSimulation successful.
[info] Simulation(s) execution ended.
[success] Total time: 8 s, completed Mar 15, 2017 12:47:52 PM
Extended report information can be found in the linked report HTML file. Simulation status is output while the simulation runs, displaying the number of errors with their details, if there are any.
It can be seen that when visiting the site's root, a redirect occurred. By default, Gatling follows all redirects on an HTTP chain and labels them as "Name - Redirect #". This behavior can disabled globally, per chain, and capped. More information about redirect configuration here.
Chaining Requests #
In the example simulation above, there was a single request to a single page. For useful simulations, there will likely be more than one request. As each call to exec returns the modified scenario object, scenarios can be chained to perform more complex behaviors.
// ...
val scn = scenario("Scenario Name")
.exec(http("Main Page")
.get("/"))
.exec(http("Search")
.get("/?q=Some+Query"))
// ...
This scenario now also runs a search, visiting https://google.com/?q=Some+Query. Cookies and other session data are retained and passed between each exec call. Redirects will still be followed at each step, then continued on the next step. The HTTP protocol supports POST requests, cookies, redirects, and more complex configurations with proxies. More on this later.
Assertions #
Assertions can be used to verify that responses contain expected content. One of the most basic assertions is for status codes. For example, if we we wanted our scenario to verify that the Google main page returned HTTP200:
// ...
val scn = scenario("Scenario Name")
.exec(http("Main Page")
.get("/")
.check(status.is(200)))
.exec(http("Search")
.get("/?q=Some+Query"))
// ...
If there was not an HTTP200, the scenario would print the name of the request, 'Main Page'
, and state
the expected and found status codes. This would count as a KO in the statistics output as well:
...
> Global (OK=1 KO=1 )
> Main Page (OK=0 KO=1 )
> Search (OK=1 KO=0 )
...
Which signifies that this particular request failed to pass validation. Note that the simulation continues even though there was a failure at a prior step. There are methods which work around this, which can be useful when authentication fails.
CSS selectors are also used to verify content on pages. For example, if we wanted to ensure
that the main page had an html
node with the attribute size
set to mobile
:
// ...
val scn = scenario("Scenario Name")
.exec(http("Main Page")
.get("/")
.check(status.is(200))
.check(css("html[size]", "mobile").exists))
// ...
A great deal of other assertions exist for optionality, performance, header values, performance requirements, etc. More details are available here.
Session Modification and Inspection #
Sessions are implicitly passed between each exec call. They can manually inspected or modified during scenario definition by executing a custom function with exec.
// ...
val scn = scenario("Scenario Name")
.exec(http("Main Page")
.get("/"))
.exec(session => {
println(session)
session
})
.exec(session => session.set("hello", "bla"))
.exec(session => {
println(session)
session
})
// ...
Above is the same scenario as our first one. However, this scenario prints the session, sets "hello" in the session to "bla", then prints it again. Note: if you are not sure why session is being repeated after printing it, try to only print it and see what happens.
Here is the output:
Simulation simpleexample.ExampleSimulation started...
Session(Scenario Name,
1,
Map(
gatling.http.cache.dns -> io.gatling.http.resolver.ShuffleJdkNameResolver@582701e2,
gatling.http.cache.redirects -> io.gatling.core.util.cache.Cache@2c5ead52,
gatling.http.cookies -> CookieJar(
Map(
CookieKey(nid,google.com,/) -> StoredCookie(NID=99=SkqwEzuPt4c2J-cwZMPjX0rPxHeZHWTiDaeyEvQr-82Dh0NlSie_-trLh9SO9OfYVed4nShyQv0wFIWUnyCDsshfjsEgE0M5eYT_W3Hn2Jm20vxV56nyq4q7uRvM96bP8qnL_HPAfiMH2fXb4w; domain=.google.com; path=/; HTTPOnly,false,false,1489611434846))),
gatling.http.referer -> https://www.google.com/),
1489611433728,
42,
OK,
List(),
io.gatling.core.protocol.ProtocolComponentsRegistry$$Lambda$149/418118518@c4d1ae6)
Session(Scenario Name,
1,
Map(
gatling.http.cache.dns -> io.gatling.http.resolver.ShuffleJdkNameResolver@582701e2,
gatling.http.cache.redirects -> io.gatling.core.util.cache.Cache@2c5ead52,
gatling.http.cookies -> CookieJar(Map(
CookieKey(nid,google.com,/) -> StoredCookie(NID=99=SkqwEzuPt4c2J-cwZMPjX0rPxHeZHWTiDaeyEvQr-82Dh0NlSie_-trLh9SO9OfYVed4nShyQv0wFIWUnyCDsshfjsEgE0M5eYT_W3Hn2Jm20vxV56nyq4q7uRvM96bP8qnL_HPAfiMH2fXb4w; domain=.google.com; path=/; HTTPOnly,false,false,1489611434846))),
gatling.http.referer -> https://www.google.com/,
"hello" -> "bla"),
1489611433728,
42,
OK,
List(),
io.gatling.core.protocol.ProtocolComponentsRegistry$$Lambda$149/418118518@c4d1ae6)
In between the first and second session output, I set "hello" to "bla" which can be see in the second session. This is the primary mechanism to inspect and modify the session from the level of scenario definition.
POST Requests and Session Access #
POST requests typically make use of the saveAs assertion in order to retain response details from a previous server response. However, this doesn't always have to be the case. Here's a contrived simulation below for the sake of an easy example:
// ...
val scn = scenario("Scenario Name")
.exec(http("Login Form")
.get("/login")
.check(status.is(200)))
.check(css("form[action]", "value").saveAs("form_action"))
.check(css("input[name='csrf_token']", "value").saveAs("csrf_token"))
.exec(http("Login (POST)")
.post("${form_action}")
.formParam("csrf_token", "${csrf_token}")
.formParam("login_username", "someuser")
.formParam("login_password", "somepassword")
.check(status.is(200)))
// ...
A few new things here; let's begin with the post
HTTP chain method. This method behaves similarly
to get
, but typically uses some sort of input. There are
other methods which allow the use
of Map
s, but here we will manually build our request data using formParam
. These parameters are
sent to the url specified in the post
method's first argument as a response using the keys and
values specified.
In this example the form's action value was captured and saved into "form_action" on the session.
The next POST request then read from the session using a bash-like syntax, ${keyname}
. This reads
the key from the session Map
directly, and will print warning if it's not found. Once the POST is complete,
the status will be checked to verify the server responded with HTTP200.
Further information about the HTTP protocol is also available on the official documentation page. Information regarding the session object be found here.
Distribution #
Gatling plugins exist for both sbt and Gradle. Both work very well, however the sbt plugin is an official extension to Gatling and contains the most comprehensive documentation and examples. Unless Gatling is being added to an existing Gradle project, I would highly recommend using the sbt extension. As the official documentation does not explicitly state this, here is a minimal collection of files to get an sbt project running:
Your project tree, for a standalone sbt project, should look similar to this:
├── build.sbt
├── project
│ ├── build.properties
│ └── plugins.sbt
└── src
└── test
├── resources
└── scala
└── simpleexample
└── ExampleSimulation.scala
build.sbt
enablePlugins(GatlingPlugin)
scalaVersion := "2.11.8"
scalacOptions := Seq(
"-encoding", "UTF-8", "-target:jvm-1.8", "-deprecation",
"-feature", "-unchecked", "-language:implicitConversions", "-language:postfixOps")
libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.4" % "test,it"
libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.4" % "test,it"
addCompilerPlugin("org.psywerx.hairyfotr" %% "linter" % "0.1.17")
project/build.properties
sbt.version=0.13.13
project/plugins.sbt
addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.1")
Once the above files are set up, install sbt and run:
$ sbt
Then simulations may be run either from the sbt shell:
$ sbt
> gatling:testOnly somepackage.GeneralBrowse
Or, instead, from your shell:
$ sbt "gatling:testOnly somepackage.GeneralBrowse"
The reports can be found in /target/gatling/
and the last report can be opened in a web browser
with:
$ sbt gatling:lastReport
A list of all sbt tasks are available here.
Reference Material #
- Previous: Markov Chain Speech Generation Part 1: Background
- Next: React Pitfalls