Friday, December 30, 2016

Gatling structure with Example

Basically, Gatling structure can be defined in 4 different parts:
  1. HTTP protocol configuration – This will define your base URL that you will be running your tests against. Also, you can define some other configurations such as user agent, language header, connection and so on.
  2. Headers definition – This provides the headers for each request that will be sent to the server. This is relevant because the headers also add a bit of load to the server you are testing.
  3. Scenario definition - The core of your test! A Scenario is a group of actions (GET, POST, etc.) that will be executed in order to simulate a user interaction with your application.
  4. Simulation definition - This defines the load (amount of users) that will concurrently execute your scenario for a period of time.
Let's imagine a simple web application where users can upload text files with some data. The app provides a button that the user can hit to verify if the data uploaded matches with any data in the system.
HTTP protocol configuration:
Firstly, we will add some basic imports and start defining our HTTP Configuration. We set our baseURL to http:://my-application:8080. This means that every time we run a request, it will use our baseURL following by the other configurations such as acceptHeaderacceptEncodingHeaderacceptLanguageHeaderconnection and userAgentHeader.
package com.myapplication.app
import com.excilys.ebi.gatling.core.Predef._
import com.excilys.ebi.gatling.http.Predef._
import assertions._
class UploadFileScenario extends Simulation {
  val httpConf = httpConfig
    .baseURL("http://my-application:8080")
    .acceptHeader("image/png,image/*;q=0.8,*/*;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .connection("keep-alive")
    .userAgentHeader("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36")
After that, we will define a scenario called "Upload a file". This scenario will execute a group of actions. The first action will be a GET to the homepage. The second one will be a POST with an attachment of a .txt file. Note that we are using two interesting commands here: check() and saveAs(). Basically what they do is this - after we POST our data, we use check() to verify the response of it. Considering we need a fileId for the next request, we are parsing the fileId (through a Regular Expression) and then using saveAs() to store this data in a variable called fileId.

Headers definition:
    val scn = scenario("Upload a file")
    .exec(http("homepage_GET")
        .get("/my-application/app/service/workgroup/latest")
        .header("Content-Type", "application/json")
    )
    .exec(http("attach_txt_file_POST")
        .post("/my-application/app/service/file/upload/")
        .header("Accept", "text/html")
        .upload("myFile", "file.txt", "text/plain")
        .check(regex("<div id=\'fileId\'>(.*?)<\/div>")
        .saveAs("fileId"))
    )
Our third action is another POST. As you can see below, we are now passing our fileId as a parameter of our body. Also, we use check() again to verify our response and parse it through JSONPath and then using saveAs() to save workgroupId for our next step.

Scenario definition:
    .exec(http("upload_txt_file_POST")
        .post("/my-application/app/service/workgroup/")
        .body( """{"name": "my-perf-test-${fileId}", "fileId": "${fileId}"}""").asJSON
        .check(jsonPath("workgroupId")
        .saveAs("workgroupId"))
    )
In the last action of our flow, we are doing one more GET and passing our previous captured workgroupId as a parameter.
.exec(http("run_content_match_GET")
.get("/my-application/app/service/workgroup/contentmatch/${workgroupId}")
.header("Content-Type", "application/json")
)
Ok, now we have our flow defined: we access our homepage, we attach a .txt file, we upload it and then run another action called content match. So, what do we want to test here? That's what we need to tell Gatling through setUp() and assertThat(). In our setUp() we are telling Gatling: "Hey, please simulate my whole scenario for 10 users in 30 seconds". It basically means that our "users" will start interacting with our application progressively. In this case, after 3 seconds a new user will start doing our flow.

Simulation definition:
setUp(
    scn.users(10).ramp(30).protocolConfig(httpConf)
)

More users = more load!

So, this is great, we can load test our server with... one user! We are going to increase the number of users to show you another great feature of Gatling: feeders.

Increasing the number of users

To increase the number of simulated users, all you have to do is to change the configuration of the simulation as follows:
scn.users(10).protocolConfig(httpConf)

Ramping

If you want to simulate 3 000 users, you don’t want them to start at the same time. Indeed, they are more likely to connect to your web application gradually.

Gatling provides the ramp option to implement this behavior. The value of the ramp indicates the duration over which the users will be linearly started. Just like every duration in Gatling, the default unit is second.

scn.users(10).ramp(10) // 10 users/10s = 1 user/s
scn.users(10).ramp(20) // 10 users/20s = 0.5 user/s = 1 user every 2s
scn.users(1000).ramp(100) // 1000 users/100s = 10 users/s
In our scenario, we will set a ramp of 10 seconds.

Dynamic values with Feeders

We have set our simulation to run 10 users, but they all use the same credentials. Wouldn’t it be nice if every user could use its own credentials? This is where Feeders will be useful.
Feeders are data sources containing all the values you want to use in your scenarios. There are several types of Feeders, the simpliest being the CSV Feeder: this is the one we will use in our test. 
Here are the feeder we use and the modifications we made to our scenario:
/* user_information.csv */
username,password,account_id
user1,password1,4
user2,password2,7
...
user10,password10,34
/* Scenario */
.feed(csv("user_information.csv"))
.exec(
  http("request_3")
    .post("/login")
    .param("username", "${username}")
    .param("password", "${password}")
    .headers(headers_3)
    .check(status.is(302))
)
...
.exec(
  http("request_5")
    .get("/google/ACC${account_id}/login.html")
    .headers(headers_5)
)

Repeating actions

Most of the users of web applications will repeat actions while navigating. To represent this behavior, Gatling brings loops to you. These loops can be conditioned by either a number of repetitions, a time limit or a condition:
repeat(10) { // Will repeat the actions 10 times
    exec( http(...) ... )
    .pause(...)
}
OR
.during(20 seconds) { // Will repeat the actions for at least 20s
    exec( http(...) ... )
    .pause(...)
}
Finally, we need to define our expected results. Gatling provides a good amount of assertions to help us with that. In this case we are asserting that none of our requests will fail. We could say for instance: "I want to make sure all my requests won't take more than X seconds".
assertThat(
    global.failedRequests.count.is(0)
)
If you would like to see the full code example above, you can take a look ..
package com.myapplication.app
import com.excilys.ebi.gatling.core.Predef._
import com.excilys.ebi.gatling.http.Predef._
import assertions._

class UploadFileScenario extends Simulation {
  val httpConf = httpConfig
    .baseURL("http://my-application:8080")
    .acceptHeader("image/png,image/*;q=0.8,*/*;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .connection("keep-alive")
    .userAgentHeader("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36")

    val scn = scenario("Upload a file")

    .exec(http("homepage_GET")
        .get("/my-application/app/service/workgroup/latest")
        .header("Content-Type", "application/json")
        )

    .exec(http("attach_txt_file_POST")
        .post("/my-application/app/service/file/upload/")
        .header("Accept", "text/html")
        .upload("myFile", "file.txt", "text/plain")
        .check(regex("<div id=\'fileId\'>(.*?)<\\/div>")
        .saveAs("fileId"))
        )

    .exec(http("upload_txt_file_POST")
        .post("/my-application/app/service/workgroup/")
        .body( """{"name": "my-perf-test-${fileId}", "fileId": "${fileId}"}""").asJSON
        .check(jsonPath("workgroupId")
        .saveAs("workgroupId"))
        )

    .exec(http("run_content_match_GET")
        .get("/my-application/app/service/workgroup/contentmatch/${workgroupId}")
        .header("Content-Type", "application/json")
        )

    setUp(
        scn.users(10).ramp(30).protocolConfig(httpConf)
        )

    assertThat(
        global.failedRequests.count.is(0)
        )
}
It's nice that Gatling can be used not just for simulating user load, but also to check HTTP Responses. You can use Gatling Assertions to verify if your requests returned a 404 code or an expected result such as a specific JSON value, for instance. Gatling allows you to use Regular Expressions, CSS Selectors, XPath and JSONPath to parse your response and extract the information you are interested in and even use it further in your next steps! For more information about Gatling DSL, I recommend you take a look at this awesome cheat sheet.
If you are curious to see how a Report looks like, here is an example (of course, there are many other views):
It is worth to mention that Chrome Dev Tools come in handy to help you identifying GETs, POSTs or whatever it is that you want to simulate. The "Network tab" is extremely helpful in this case. I would also recommend you to use cURL as a support tool. It can help you to simulate easily your requests before you map them to inside your Gatling test scenarios.
Bonus tip: In Chrome Dev Tools (in the Network tab) you can right click on the request you are interested and select the option "Copy as cURL" ;)
Gatling also provides you a GUI, but you definitely do not need it due to the simplicity of its DSL.