Gatling Load Testing #2
Intro
In this blog post we gonna go a litlle bit futher that we went on the previous blog post on Gatling. This is why I hightly recomment you having a read on the first gatling blog post.
For this article we gonna use the JPetStore from octoperf : https://petstore.octoperf.com/
And we gonna create a simple scenario that :
- Login (with a previously created account)
- Browse some items
- Add some items to cart
- Place order
Record your scenario
We are using the recording method that we explained in the previous blog post
After recording we can generate a file that looks like this:
package priolix
import scala.concurrent.duration._
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
class Scenario1 extends Simulation {
val httpProtocol = http
.baseUrl("https://petstore.octoperf.com")
.inferHtmlResources(BlackList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*detectportal\.firefox\.com.*"""), WhiteList())
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("en-US,en;q=0.9")
.upgradeInsecureRequestsHeader("1")
.userAgentHeader("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36")
val headers_0 = Map(
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "none",
"sec-fetch-user" -> "?1")
val headers_1 = Map(
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "same-origin",
"sec-fetch-user" -> "?1")
val headers_3 = Map(
"origin" -> "https://petstore.octoperf.com",
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "same-origin",
"sec-fetch-user" -> "?1")
val scn = scenario("Scenario1")
.exec(http("request_0")
.get("/")
.headers(headers_0))
.pause(2)
.exec(http("request_1")
.get("/actions/Catalog.action")
.headers(headers_1))
.pause(3)
.exec(http("request_2")
.get("/actions/Account.action;jsessionid=6D5E6EF2EFBE77CC3EAB7DFCA53D3726?signonForm=")
.headers(headers_1))
.pause(6)
.exec(http("request_3")
.post("/actions/Account.action")
.headers(headers_3)
.formParam("username", "priolix")
.formParam("password", "qwerty")
.formParam("signon", "Login")
.formParam("_sourcePage", "V5VUaHjz2gsPgTRtw2vLQ2EjQT_DK1EM6AEQPaTu4vgqfhO7PKwpPyQZGN_7GvyL48-UITYMygV-E-uqoF5Q6p_auRR7IurLk_hgKxRD5hI=")
.formParam("__fp", "knMJ5sRUcFhvDut0kVxALcuyYld3Ul39yNAwRZE9Lg5FSVGg0LJ7v8D3STtJ4Qli"))
.pause(2)
.exec(http("request_4")
.get("/actions/Catalog.action?viewCategory=&categoryId=FISH")
.headers(headers_1))
.pause(1)
.exec(http("request_5")
.get("/actions/Catalog.action?viewProduct=&productId=FI-FW-01")
.headers(headers_1))
.pause(1)
.exec(http("request_6")
.get("/actions/Catalog.action?viewCategory=&categoryId=REPTILES")
.headers(headers_1))
.pause(1)
.exec(http("request_7")
.get("/actions/Catalog.action?viewProduct=&productId=RP-LI-02")
.headers(headers_1))
.pause(2)
.exec(http("request_8")
.get("/actions/Catalog.action?viewItem=&itemId=EST-13")
.headers(headers_1))
.pause(2)
.exec(http("request_9")
.get("/actions/Cart.action?addItemToCart=&workingItemId=EST-13")
.headers(headers_1))
.pause(2)
.exec(http("request_10")
.post("/actions/Cart.action")
.headers(headers_3)
.formParam("EST-13", "1")
.formParam("updateCartQuantities", "Update Cart")
.formParam("_sourcePage", "lO0hhlBQwMTUTxWdy7RxWyJUMHz7T3To36GYjL_8j7vDK0Ih2A_VZk6MP6uREzAICl-qVveBNy6exYf3CDKulsma81j2WXjz")
.formParam("__fp", "YDrH07KQKoJ6nTKRNV0XH2EkuDKLbbGQaL0R2HcM3BMTCzgIae2YsbntWzI2zpMN")
.resources(http("request_11")
.get("/actions/Order.action?newOrderForm=")
.headers(headers_1)))
.pause(1)
.exec(http("request_12")
.post("/actions/Order.action")
.headers(headers_3)
.formParam("order.cardType", "Visa")
.formParam("order.creditCard", "999 9999 9999 9999")
.formParam("order.expiryDate", "12/03")
.formParam("order.billToFirstName", "John")
.formParam("order.billToLastName", "Pool")
.formParam("order.billAddress1", "01234 Rose Ave")
.formParam("order.billAddress2", "Unit 6")
.formParam("order.billCity", "Santa Barbara")
.formParam("order.billState", "CA")
.formParam("order.billZip", "90066")
.formParam("order.billCountry", "USA")
.formParam("newOrder", "Continue")
.formParam("_sourcePage", "7X-gLslMAaT4fw2QA-qWf_OLwmEm21tSFMEcqL3xQ1sgL52k0V3teUlOGLlp-qTNTLynYe6pCEPEzmLRZu6HxUMdNdvcrEvyIOPYwzEHCbU=")
.formParam("__fp", "OJa0cbS-5HObH0EzMq1d8zP5E6tZkKKnQgwO1Sl11y9AJVu8UAzw6R2h1KKpz4XsElQdU6tVx_X0ZEhIXOC3kOaKVb0p81uQ2qW6TsKh6p7iOPl_z83ocg=="))
.pause(1)
.exec(http("request_13")
.get("/actions/Order.action?newOrder=&confirmed=true")
.headers(headers_1))
.pause(2)
.exec(http("request_14")
.get("/actions/Account.action?editAccountForm=")
.headers(headers_1))
.pause(1)
.exec(http("request_15")
.get("/actions/Order.action?listOrders=")
.headers(headers_1))
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
Find and add POST/GET parameters
We immediatly noticing that some parameters has been gaven by the website and we need to retreive them to reinject them in our scenarios.
Like :
jsessionid=6D5E6EF2EFBE77CC3EAB7DFCA53D3726
or
.formParam("__fp", "knMJ5sRUcFhvDut0kVxALcuyYld3Ul39yNAwRZE9Lg5FSVGg0LJ7v8D3STtJ4Qli")
This first parameter is in fact a cookie that have been returned by the store.
Gatling can deal directly with cookies. This code copies the JSESSIONID cookie in the session to a variable.
.exec(getCookieValue(CookieKey("JSESSIONID").withPath("/").saveAs("JSESSIONID")))
Then we can exploit it like that :
.exec(http("request_2")
.get("/actions/Account.action;jsessionid=${JSESSIONID}?signonForm=")
.headers(headers_1))
By using methods that are described bellow we can figure out that the __fp
and _sourcePage
are given by the application before submiting it.
We can parse the previous page and store the parameter into a variable like that :
.exec(http("request_2")
.get("/actions/Account.action;jsessionid=${JSESSIONID}?signonForm=")
.headers(headers_1)
.check(regex("""name="_sourcePage" value="([^"]+)"""").saveAs("_sourcePage"))
.check(regex("""name="__fp" value="([^"]+)"""").saveAs("__fp")))
And we reusing like that :
.exec(http("request_3")
.post("/actions/Account.action")
.headers(headers_3)
.formParam("username", "priolix")
.formParam("password", "qwerty")
.formParam("signon", "Login")
.formParam("_sourcePage", "${_sourcePage}")
.formParam("__fp", "${__fp}"))
The final code looks like this :
package priolix
import scala.concurrent.duration._
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
class Scenario1 extends Simulation {
val httpProtocol = http
.baseUrl("https://petstore.octoperf.com")
.inferHtmlResources(BlackList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*detectportal\.firefox\.com.*"""), WhiteList())
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("en-US,en;q=0.9")
.upgradeInsecureRequestsHeader("1")
.userAgentHeader("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36")
val headers_0 = Map(
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "none",
"sec-fetch-user" -> "?1")
val headers_1 = Map(
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "same-origin",
"sec-fetch-user" -> "?1")
val headers_3 = Map(
"origin" -> "https://petstore.octoperf.com",
"pragma" -> "no-cache",
"sec-ch-ua" -> """Chromium";v="91", " Not;A Brand";v="99""",
"sec-ch-ua-mobile" -> "?0",
"sec-fetch-dest" -> "document",
"sec-fetch-mode" -> "navigate",
"sec-fetch-site" -> "same-origin",
"sec-fetch-user" -> "?1")
val scn = scenario("Scenario1")
.exec(http("request_0")
.get("/")
.headers(headers_0))
.pause(2)
.exec(http("request_1")
.get("/actions/Catalog.action")
.headers(headers_1))
.pause(3)
.exec(getCookieValue(CookieKey("JSESSIONID").withPath("/").saveAs("JSESSIONID")))
.exec(http("request_2")
.get("/actions/Account.action;jsessionid=${JSESSIONID}?signonForm=")
.headers(headers_1)
.check(regex("""name="_sourcePage" value="([^"]+)"""").saveAs("_sourcePage"))
.check(regex("""name="__fp" value="([^"]+)"""").saveAs("__fp")))
.pause(6)
.exec(http("request_3")
.post("/actions/Account.action")
.headers(headers_3)
.formParam("username", "priolix")
.formParam("password", "qwerty")
.formParam("signon", "Login")
.formParam("_sourcePage", "${_sourcePage}")
.formParam("__fp", "${__fp}"))
.pause(2)
.exec(http("request_4")
.get("/actions/Catalog.action?viewCategory=&categoryId=FISH")
.headers(headers_1))
.pause(1)
.exec(http("request_5")
.get("/actions/Catalog.action?viewProduct=&productId=FI-FW-01")
.headers(headers_1))
.pause(1)
.exec(http("request_6")
.get("/actions/Catalog.action?viewCategory=&categoryId=REPTILES")
.headers(headers_1))
.pause(1)
.exec(http("request_7")
.get("/actions/Catalog.action?viewProduct=&productId=RP-LI-02")
.headers(headers_1))
.pause(2)
.exec(http("request_8")
.get("/actions/Catalog.action?viewItem=&itemId=EST-13")
.headers(headers_1))
.pause(2)
.exec(http("request_9")
.get("/actions/Cart.action?addItemToCart=&workingItemId=EST-13")
.headers(headers_1)
.check(regex("""name="_sourcePage" value="([^"]+)"""").saveAs("_sourcePage"))
.check(regex("""name="__fp" value="([^"]+)"""").saveAs("__fp")))
.pause(2)
.exec(http("request_10")
.post("/actions/Cart.action")
.headers(headers_3)
.formParam("EST-13", "1")
.formParam("updateCartQuantities", "Update Cart")
.formParam("_sourcePage", "${_sourcePage}")
.formParam("__fp", "${__fp}")
.resources(http("request_11")
.get("/actions/Order.action?newOrderForm=")
.headers(headers_1))
.check(regex("""name="_sourcePage" value="([^"]+)"""").saveAs("_sourcePage"))
.check(regex("""name="__fp" value="([^"]+)"""").saveAs("__fp")))
.pause(1)
.exec(http("request_12")
.post("/actions/Order.action")
.headers(headers_3)
.formParam("order.cardType", "Visa")
.formParam("order.creditCard", "999 9999 9999 9999")
.formParam("order.expiryDate", "12/03")
.formParam("order.billToFirstName", "John")
.formParam("order.billToLastName", "Pool")
.formParam("order.billAddress1", "01234 Rose Ave")
.formParam("order.billAddress2", "Unit 6")
.formParam("order.billCity", "Santa Barbara")
.formParam("order.billState", "CA")
.formParam("order.billZip", "90066")
.formParam("order.billCountry", "USA")
.formParam("newOrder", "Continue")
.formParam("_sourcePage", "${_sourcePage}")
.formParam("__fp", "${__fp}"))
.pause(1)
.exec(http("request_13")
.get("/actions/Order.action?newOrder=&confirmed=true")
.headers(headers_1))
.pause(2)
.exec(http("request_14")
.get("/actions/Account.action?editAccountForm=")
.headers(headers_1))
.pause(1)
.exec(http("request_15")
.get("/actions/Order.action?listOrders=")
.headers(headers_1))
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
Debug
In this section we gonna see differents methods to analyse how the website works.
The goal is to find where the differents parameters and variables are set by the website in order to reuse it dynamically.
Gatling debuging
/opt/gatling/conf/logback.xml
<!-- uncomment and set to TRACE to log all HTTP requests -->
<!--<logger name="io.gatling.http.engine.response" level="TRACE" />-->
<!-- uncomment to log WebSocket events -->
<!--<logger name="io.gatling.http.action.ws.fsm" level="DEBUG" />-->
<!-- uncomment to log SSE events -->
<!--<logger name="io.gatling.http.action.sse.fsm" level="DEBUG" />-->
Network debuging
If we have access to the target server and this server is behind a HTTPs proxy we can use TCPDump on it to extract some informations.
That can help us to find where some parameters are set and to extract them to reuse it.
tcpdump -i any -n -v -A port 80
We can also use tools like reverse proxy HTTP(s) such as :
And finaly the nework analyser from your browser is also a good help.
Merge from multiple sources
When we launch Gatling from multiple source in order to load balance the work between the injetors we might need to merge all the results into one report.
This is how we proceed.
Copy files from injector 1 and 2 in a same directory
/opt/gatling/results/merged1/# ls -lah
total 8.0K
drwxr-xr-x 2 root root 4.0K Feb 28 23:02 .
drwxr-xr-x 8 root root 4.0K Feb 28 19:34 ..
-rw-r--r-- 1 root root 0 Feb 28 23:02 1.log
-rw-r--r-- 1 root root 0 Feb 28 23:02 2.log
Then lauch gatling report generator:
gatling.sh -ro merged1
Conclusion
In this blog post we have seen how to develop a more complex scenario with retrived parameters.
In the next Gatling blog post we gonna see how to handle lists in order on loggin with many accounts and to browse and buy random articles.
Stay tunned.