Performance testing with Gatling – advanced usage

Last Updated on by

Post summary: Code samples and explanation how to do advanced performance testing with Gatling, such as proper scenarios structure, checks, feeding test data, session maintenance, etc.

Current post is part of Performance testing with Gatling series in which Gatling performance testing tool is explained in details.

Code samples are available in GitHub sample-performance-with-gatling repository.

In previous post Performance testing with Gatling – integration with Maven there is description how to setup Maven project. In Performance testing with Gatling – recorded simulation explanation there is information what a simulation consists of. Simulations from this post will be refactored in current post and more advanced topics will be discussed how to make flexible automation testing with Gatling.

What is included

Following topics are included in the current post:

  • Access external configuration data
  • Define single HTTP requests for better re-usability
  • Add checks for content on HTTP response
  • Check and extract data from HTTP response
  • More checks and extract List with values
  • Create HTTP POST request with the body from a template file
  • Manage session variables
  • Standard CSV feeder
  • Create custom feeder
  • Create unified scenarios
  • Conditional scenario execution
  • Only one HTTP protocol
  • Extract data from HTTP request and response
  • Advanced simulation setUp
  • Virtual users vs requests per second

Refactored code

Below are all classes that are created after they have been refactored. In order to separate things and make it easier to read ProductSimulation and PersonSimulation classes contain only the setUp() method. Request, scenarios and external configurations are being defined into Constants, Product and Person singleton objects.

Constants

object Constants {
	val numberOfUsers: Int = System.getProperty("numberOfUsers").toInt
	val duration: FiniteDuration = System.getProperty("durationMinutes").toInt.minutes
	val pause: FiniteDuration = System.getProperty("pauseBetweenRequestsMs").toInt.millisecond
	val responseTimeMs = 500
	val responseSuccessPercentage = 99
	private val url: String = System.getProperty("url")
	private val repeatTimes: Int = System.getProperty("numberOfRepetitions").toInt
	private val successStatus: Int = 200
	private val isDebug = System.getProperty("debug").toBoolean

	val httpProtocol = http
		.baseURL(url)
		.check(status.is(successStatus))
		.extraInfoExtractor { extraInfo => List(getExtraInfo(extraInfo)) }

	def createScenario(name: String, feed: FeederBuilder[_], chains: ChainBuilder*): ScenarioBuilder = {
		if (Constants.repeatTimes > 0) {
			scenario(name).feed(feed).repeat(Constants.repeatTimes) {
				exec(chains).pause(Constants.pause)
			}
		} else {
			scenario(name).feed(feed).forever() {
				exec(chains).pause(Constants.pause)
			}
		}
	}

	private def getExtraInfo(extraInfo: ExtraInfo): String = {
		if (isDebug
			|| extraInfo.response.statusCode.get != successStatus
			|| extraInfo.status.eq(Status.apply("KO"))) {
			",URL:" + extraInfo.request.getUrl +
				" Request: " + extraInfo.request.getStringData +
				" Response: " + extraInfo.response.body.string
		} else {
			""
		}
	}
}

Product

object Product {

	private val reqGoToHome = exec(http("Open home page")
		.get("/products")
		.check(regex("Search: "))
	)

	private val reqSearchProduct = exec(http("Search product")
		.get("/products?q=${search_term}&action=search-results")
		.check(regex("Your search for '${search_term}' gave ([\\d]{1,2}) results:").saveAs("numberOfProducts"))
		.check(regex("NotFound").optional.saveAs("not_found"))
	)

	private val reqOpenProduct = exec(session => {
		var numberOfProducts = session("numberOfProducts").as[String].toInt
		var productId = Random.nextInt(numberOfProducts) + 1
		session.set("productId", productId)
	}).exec(http("Open Product")
		.get("/products?action=details&id=${productId}")
		.check(regex("This is 'Product ${productId} name' details page."))
	)
	
	private val csvFeeder = csv("search_terms.csv").circular.random

	val scnSearch = Constants.createScenario("Search", csvFeeder,
		reqGoToHome, reqSearchProduct, reqGoToHome)

	val scnSearchAndOpen = Constants.createScenario("Search and Open", csvFeeder,
		reqGoToHome, reqSearchProduct, reqOpenProduct, reqGoToHome)
}

ProductSimulation

class ProductSimulation extends Simulation {

	setUp(
		Product.scnSearch.inject(rampUsers(Constants.numberOfUsers) over 10.seconds),
		Product.scnSearchAndOpen.inject(atOnceUsers(Constants.numberOfUsers))
	)
		.protocols(Constants.httpProtocol.inferHtmlResources())
		.pauses(constantPauses)
		.maxDuration(Constants.duration)
		.assertions(
			global.responseTime.max.lessThan(Constants.responseTimeMs),
			global.successfulRequests.percent.greaterThan(Constants.responseSuccessPercentage)
		)
}

Person

object Person {

	private val added = "Added"
	private val updated = "Updated"

	private val reqGetAll = exec(http("Get All Persons")
		.get("/person/all")
		.check(regex("\"firstName\":\"(.*?)\"").count.greaterThan(1).saveAs("count"))
		.check(regex("\\[").count.is(1))
		.check(regex("\"id\":([\\d]{1,6})").findAll.saveAs("person_ids"))
	).exec(session => {
		val count = session("count").as[Int]
		val personIds = session("person_ids").as[List[Int]]
		val personId = personIds(Random.nextInt(count)).toString.toInt
		session.set("person_id", personId)
	}).exec(session => {
		println(session)
		session
	})

	private val reqGetPerson = exec(http("Get Person")
		.get("/person/get/${person_id}")
		.check(regex("\"firstName\":\"(.*?)\"").count.is(1))
		.check(regex("\\[").notExists)
	)

	private val reqSavePerson = exec(http("Save Person")
		.post("/person/save")
		.body(ElFileBody("person.json"))
		.header("Content-Type", "application/json")
		.check(regex("Person with id=([\\d]{1,6})").saveAs("person_id"))
		.check(regex("\\[").notExists)
		.check(regex("(" + added + "|" + updated + ") Person with id=").saveAs("action"))
	)

	private val reqGetPersonAferSave = exec(http("Get Person After Save")
		.get("/person/get/${person_id}")
		.check(regex("\"id\":${person_id}"))
		.check(regex("\"firstName\":\"${first_name}\""))
		.check(regex("\"lastName\":\"${last_name}\""))
		.check(regex("\"email\":\"${email}\""))
	)

	private val reqGetPersonAferUpdate = exec(http("Get Person After Update")
		.get("/person/get/${person_id}")
		.check(regex("\"id\":${person_id}"))
	)

	private val uniqueIds: List[String] = Source
		.fromInputStream(getClass.getResourceAsStream("/account_ids.txt"))
		.getLines().toList

	private val feedSearchTerms = Iterator.continually(buildFeeder(uniqueIds))

	private def buildFeeder(dataList: List[String]): Map[String, Any] = {
		Map(
			"id" -> (Random.nextInt(100) + 1),
			"first_name" -> Random.alphanumeric.take(5).mkString,
			"last_name" -> Random.alphanumeric.take(5).mkString,
			"email" -> Random.alphanumeric.take(5).mkString.concat("@na.na"),
			"unique_id" -> dataList(Random.nextInt(dataList.size))
		)
	}

	val scnGet = Constants.createScenario("Get all then one", feedSearchTerms,
		reqGetAll, reqGetPerson)

	val scnSaveAndGet = Constants.createScenario("Save and get", feedSearchTerms, reqSavePerson)
		.doIfEqualsOrElse("${action}", added) {
			reqGetPersonAferSave
		} {
			reqGetPersonAferUpdate
		}
}

PersonSimulation

class PersonSimulation extends Simulation {

	setUp(
		Person.scnGet.inject(atOnceUsers(Constants.numberOfUsers)),
		Person.scnSaveAndGet.inject(atOnceUsers(Constants.numberOfUsers))
	)
		.protocols(Constants.httpProtocol)
		.pauses(constantPauses)
		.maxDuration(Constants.duration)
		.assertions(
			global.responseTime.max.lessThan(Constants.responseTimeMs),
			global.successfulRequests.percent.greaterThan(Constants.responseSuccessPercentage)
		)
}

Access external configuration data

In order to have flexibility it is mandatory to be able to sent different configurations parameters from command line when invoking the scenario. With Gatling Maven plugin it is done with  configurations. See more in Performance testing with Gatling – integration with Maven post.

val numberOfUsers: Int = System.getProperty("numberOfUsers").toInt
val duration: FiniteDuration = System.getProperty("durationMinutes").toInt.minutes
private val url: String = System.getProperty("url")
private val repeatTimes: Int = System.getProperty("numberOfRepetitions").toInt
private val isDebug = System.getProperty("debug").toBoolean

Define single HTTP requests for better re-usability

It is a good idea to define each HTTP request as a separate object. This gives the flexibility to reuse one and the same requests in different scenarios. Below is shown how to create HTTP GET request with http().get().

Add checks for content on HTTP response

On HTTP request creation there is a possibility to add checks that certain string or regular expression pattern exists in response. The code below created HTTP Request and add check that “Search: “ text exists in response. This is done with regex() method by passing just a string to it.

private val reqGoToHome = exec(http("Open home page")
	.get("/products")
	.check(regex("Search: "))
)

Check and extract data from HTTP response

It is possible along with the check to extract data into a variable that is being saved to the session. This is done with saveAs() method. In some cases value we are searching for, might not be in the response. We can use optional method to specify that value is saved in session only if existing. If it is not existing it won’t be captured and this will not break the execution. As shown below session variables can be also used in the checks. Session variable is accessed with ${},  such as ${search_term}.

private val reqSearchProduct = exec(http("Search product")
	.get("/products?q=${search_term}&action=search-results")
	.check(regex("Your search for '${search_term}' gave ([\\d]{1,2}) results:")
		.saveAs("numberOfProducts"))
	.check(regex("NotFound").optional.saveAs("not_found"))
)

More checks and extract List with values

There are many types of checks. In code below count.greaterThan(1) and count.is(1) are used. It is possible to search for multiple occurrences of given regular expression with findAll. In such case saveAs() saves the results to a “person_ids” List object in session. More information about checks can be found in Gatling Checks page.

private val reqGetAll = exec(http("Get All Persons")
	.get("/person/all")
	.check(regex("\"firstName\":\"(.*?)\"").count.greaterThan(1).saveAs("count"))
	.check(regex("\\[").count.is(1))
	.check(regex("\"id\":([\\d]{1,6})").findAll.saveAs("person_ids"))
)

Create HTTP POST request with the body from a template file

If you need to post data to server HTTP POST request is to be used. The request is created with http().post() method. Headers can be added to the request with header() or headers() methods. In the current example, without Content-Type=application/json header REST service will throw an error for unrecognized content. Data that will be sent is added in body() method. It accepts Body object. You can generate body from a file (RawFileBody method) or string (StringBody method).

private val reqSavePerson = exec(http("Save Person")
	.post("/person/save")
	.body(ElFileBody("person.json"))
	.header("Content-Type", "application/json")
	.check(regex("Person with id=([\\d]{1,6})").saveAs("person_id"))
	.check(regex("\\[").notExists)
	.check(regex("(" + added + "|" + updated + ") Person with id=")
		.saveAs("action"))
)

In current case body is generated from file, which have variables that can be later on found in session. This is done with ElFileBody (ELFileBody in 2.0.0) method and actual replace with value is done by Gatling EL (expression language). More about what can you do with EL can be found on Gatling EL page. EL body file is shown below, where variables ${id}, ${first_name}, ${last_name} ${email} are searched in session and replaced if found. If not found error is shown on scenario execution output.

{
	"id": "${id}",
	"firstName": "${first_name}",
	"lastName": "${last_name}",
	"email": "${email}"
}

Manage session variables

Each virtual user has its own session. The scenario can store or read data from the session. Data is saved in session with key/value pairs, where the key is the variable name. Variables are stored in session in three ways: using feeders (this is explained later in the current post), using saveAs() method and session API. More details on session API can be found in Gatling Session API page.

Manipulating session through API is kind of tricky. Gatling documentation is vague about it. Below is shown a code where session variable is extracted first as String and then converted to Int with: var numberOfProducts = session(“numberOfProducts”).as[String].toInt. On next step some manipulation is done with this variable, in current case, a random product id from 1 to “numberOfProducts” to is picked. At last a new variable is saved in session with session.set(“productId”, productId). It is important that this is the last line of session manipulation code block done in first exec(). This is the return statement of the code block. In other words, new Session object with saved “productId” in it is returned. If on the last line is just “session” as stated in the docs, then old, an unmodified session object is returned without variable being added.

Sessions as most of the objects in Gatling and in Scala are immutable. This is designed for thread safety. So adding a variable to session actually creates a new object. This is why newly added session variable cannot be used in the same exec() block, but have to be used on next one, as in same block variable is yet not accessible. See code below in the second exec() “productId” is already available and can be used in get().

private val reqOpenProduct = exec(session => {
	var numberOfProducts = session("numberOfProducts").as[String].toInt
	var productId = Random.nextInt(numberOfProducts) + 1
	session.set("productId", productId)
}).exec(http("Open Product")
	.get("/products?action=details&id=${productId}")
	.check(regex("This is 'Product ${productId} name' details page."))
)

Same logic being explained above is implemented in next code fragment. The below example shows usage of session variable saved in previous exec() fragment. Count of persons and List with ids are being saved by saveAs() method. The list is extracted from the session and random index of it has been accessed, so random person is being selected. This is again saved into session as “person_id”. In third exec() statement “session” object is just printed to output for debugging purposes.

private val reqGetAll = exec(http("Get All Persons")
	.get("/person/all")
	.check(regex("\"firstName\":\"(.*?)\"").count.greaterThan(1).saveAs("count"))
	.check(regex("\\[").count.is(1))
	.check(regex("\"id\":([\\d]{1,6})").findAll.saveAs("person_ids"))
).exec(session => {
	val count = session("count").as[Int]
	val personIds = session("person_ids").as[List[Int]]
	val personId = personIds(Random.nextInt(count)).toString.toInt
	session.set("person_id", personId)
}).exec(session => {
	println(session)
	session
})

Standard CSV feeder

A feeder is a way to generate unique data for each virtual user. This how tests are made real. Below is a way to read data from CSV file. The first line of the CSV file is the header which is saved to the session as a variable name. In the current example, CSV has only one column, but it is possible to have CSV file with several columns. circular means that if file end is reached feeder will start from the beginning. random means elements are taken in random order. More about feeders can be found in Gatling Feeders page.

private val csvFeeder = csv("search_terms.csv").circular.random

Create custom feeder

Feeder actually is Iterator[Map[String, T]], so you can do your own feeders. Below is shown code where some unique ids are read from file and converted to List[String] with Source .fromInputStream(getClass.getResourceAsStream(“/account_ids.txt”)) .getLines().toList. This list is used in buildFeeder() method to access random element from it. Finally Iterator.continually(buildFeeder(uniqueIds)) creates infinite length iterator.

private val uniqueIds: List[String] = Source
	.fromInputStream(getClass.getResourceAsStream("/account_ids.txt"))
	.getLines().toList

private val feedSearchTerms = Iterator.continually(buildFeeder(uniqueIds))

private def buildFeeder(dataList: List[String]): Map[String, Any] = {
	Map(
		"id" -> (Random.nextInt(100) + 1),
		"first_name" -> Random.alphanumeric.take(5).mkString,
		"last_name" -> Random.alphanumeric.take(5).mkString,
		"email" -> Random.alphanumeric.take(5).mkString.concat("@na.na"),
		"unique_id" -> dataList(Random.nextInt(dataList.size))
	)
}

The current business case doesn’t make much sense to have a custom feeder with values from a file, just Map() generator is enough. But let us imagine a case where you search for a hotel by unique id and some date in the future. Hard coding date in CSV file is not a wise solution, you will want to be always in the future. Also making different combinations from hotelId, start and end dates is not possible to be maintained in a file. The best solution is to have a file with hotel ids and dates to be dynamically generated as shown in buildFeeder() method.

Create unified scenarios

The scenario is created from HTTP requests. This is why it is good to have each HTTP request as a separate object so you can reuse them in different scenarios. In order to unify scenario creation, there is a special method. It takes scenario name, feeder and list of requests and returns a scenario object. Method checks if the scenario is supposed to be repeated several times and uses repeat() method. Else scenarios are repeated forever(). In both cases, there is constant pause time introduced between requests with pause().

def createScenario(name: String, 
					feed: FeederBuilder[_],
					chains: ChainBuilder*): ScenarioBuilder = {
	if (Constants.repeatTimes > 0) {
		scenario(name).feed(feed).repeat(Constants.repeatTimes) {
			exec(chains).pause(Constants.pause)
		}
	} else {
		scenario(name).feed(feed).forever() {
			exec(chains).pause(Constants.pause)
		}
	}
}

With this approach, a method can be reused from many places avoiding duplication of code.

val scnSearch = Constants.createScenario("Search", csvFeeder,
		reqGoToHome, reqSearchProduct, reqGoToHome)

Conditional scenario execution

It is possible one scenario to have different execution paths based on a condition. This condition is generally a value of a session variable. Branching is done with doIf, doIfElse, doIfEqualsOrElse, etc methods. In the current example, if this is Save request then additional reqGetPersonAferSave HTTP request is executed. Else additional reqGetPersonAferUpdate HTTP request is executed. In the end, there is only one scenario scnSaveAndGet but it can have different execution paths based on “action” session variable.

val scnSaveAndGet = Constants
	.createScenario("Save and get", feedSearchTerms, reqSavePerson)
	.doIfEqualsOrElse("${action}", added) {
		reqGetPersonAferSave
	} {
		reqGetPersonAferUpdate
	}

Only one HTTP protocol

In general case, several performance testing simulations can be done for one and the same application. During simulation setUp an HTTP protocol object is needed. Since the application is the same HTTP protocol can be one and the same object, so it is possible to define it and reuse it. If changes are needed new HTTP protocol object can be defined or a copy of current one can be created and modified.

val httpProtocol = http
	.baseURL(url)
	.check(status.is(successStatus))
	.extraInfoExtractor { extraInfo => List(getExtraInfo(extraInfo)) }

Extract data from HTTP request and response

In order to ease debugging of failures or debugging at all, it is possible to extract information from HTTP request and response. Extraction is configured on HTTP protocol level with extraInfoExtractor { extraInfo => List(getExtraInfo(extraInfo)) } as shown above. In order to simplify code processing of extra info object is done in a separate method. If debug is enabled or response code is not 200 or Gatling status is KO then request URL, request data and response body are dumped into simulation.log file that resides in results folder. Note that response body is extracted only if there is check on it, otherwise, there is NoResponseBody in the output. This is done to improve performance.

private def getExtraInfo(extraInfo: ExtraInfo): String = {
	if (isDebug
		|| extraInfo.response.statusCode.get != successStatus
		|| extraInfo.status.eq(Status.apply("KO"))) {
		",URL:" + extraInfo.request.getUrl +
			" Request: " + extraInfo.request.getStringData +
			" Response: " + extraInfo.response.body.string
	} else {
		""
	}
}

Advanced simulation setUp

It is a good idea to keep simulation class clean by defining all objects in external classes or singleton objects. Simulation is mandatory to have setUp() method. It receives a comma-separated list of scenarios. In order scenario to be valid, it should have users injected with inject() method. There are different strategies to inject users. The protocol should also be defined per scenario setup. In this particular example default protocol is used with the change to fetch all HTTP resources on a page (JS, CSS, images, etc.) with inferHtmlResources(). Since objects are immutable this creates a copy of default HTTP protocol and does not modify the original one. Assertions is a way to verify certain performance KPI it is defined with assertions() method. In this example, we should have a response time less than 500ms and more than 99% of requests should be successful.

private val rampUpTime: FiniteDuration = 10.seconds

setUp(
	Product.scnSearch.inject(rampUsers(Constants.numberOfUsers) over rampUpTime),
	Product.scnSearchAndOpen.inject(atOnceUsers(Constants.numberOfUsers))
)
	.protocols(Constants.httpProtocol.inferHtmlResources())
	.pauses(constantPauses)
	.maxDuration(Constants.duration)
	.assertions(
		global.responseTime.max.lessThan(Constants.responseTimeMs),
		global.successfulRequests.percent
			.greaterThan(Constants.responseSuccessPercentage)
	)
	.throttle(reachRps(100) in rampUpTime, holdFor(Constants.duration))

Cookies management

Cookie support is enabled by default and then Gatling handles Cookies transparently, just like a browser would. It is possible to add or delete cookies during the simulation run. See more details how this is done in Gatling Cookie management page.

Virtual users vs requests per second

Since users are vague metric, but requests per second is metric that most server monitoring tools support it is possible to use this approach. Gatling supports so-called throttling: throttle(reachRps(100) in 10.seconds, holdFor(5.minutes)). It is important to put holdFor() method, otherwise, Gatling goes to unlimited requests per second and can crash the server. More details on simulation setup can be found on Gatling Simulation setup page.

Conclusion

Keeping Gatling code maintainable and reusable is a good practice to create complex performance scenarios. Gatling API provides a wide range of functionalities to support this task. In the current post, I have shown cases and solution to them which I have encountered in real life projects.

Related Posts

Read more...

Performance testing with Gatling – recorded simulation explanation

Last Updated on by

Post summary: Explanation of automatically generated code of recorded Gatling simulations.

Current post is part of Performance testing with Gatling series in which Gatling performance testing tool is explained in details.

Code samples are available in GitHub sample-performance-with-gatling repository.

Application under test

For current tutorial application from Build a RESTful stub server with Dropwizard post is used. It is a pretty simple application. One feature is Products web application where you can search for products, open one and see its details. The other features used in this post is Persons REST service, where you can get or save person via JSON.

Record simulation

Coding simulations from scratch can be difficult and tricky, so it is always a good idea to record the scenario and then modify it. How to record can be found in Performance testing with Gatling – record and playback post. The recording that was done on the application under test for current tutorial produced following simulation files, which can be found in com.automationrhapsody.gatling.simulations.original package of GitHub project. There are two simulations being recorded. ProductSimulation which tests web application and PersonSImulation testing REST service.

ProductSimulation explained

Below is a recorded code for product simulation:

package com.automationrhapsody.gatling.simulations.original

import io.gatling.core.Predef._
import io.gatling.http.Predef._

class ProductSimulation extends Simulation {

	val httpProtocol = http
		.baseURL("http://localhost:9000")
		.inferHtmlResources()


	val uri1 = "http://localhost:9000/products"

	val scn = scenario("RecordedSimulation")
		.exec(http("request_0")
			.get("/products"))
		.pause(11)
		.exec(http("request_1")
			.get("/products?q=SearchString&action=search-results"))
		.pause(8)
		.exec(http("request_2")
			.get("/products?action=details&id=1"))
		.pause(6)
		.exec(http("request_3")
			.get("/products"))

	setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

Simulation is performing following steps:

  • Open /products URI on http://localhost:9000 URL.
  • Wait 11 seconds.
  • Search for “SearchString”.
  • Wait 8 seconds.
  • Open product with id=1 from search results.
  • Wait 6 seconds.
  • Go to home page – /products

With val httpProtocol = http .baseURL(“http://localhost:9000”) .inferHtmlResources() an object of HTTP Protocol is instantiated. URL is configured with baseURL(). All related HTML resources are being captured with any request with inferHtmlResources(). This method allow more precise filtering what resources to be fetched and which skipped. See more in Gatling HTTP Protocol page.

Variable uri1 is defined but is not actually used anywhere, so it is redundant.

A scenario is defined with scenario(“RecordedSimulation”), this method accepts the name of the scenario, “RecordedSimulation” in the current case. A scenario is a building block of a simulation. One simulation should have at least one scenario. See more about scenarios on Gatling Scenario page.

With exec() method are added actual actions to get executed. In most of the cases, the action is an HTTP GET or POST request. In current example GET request is done with http(“request_0”) .get(“/products”), where “request_0” is the name of the HTTP Request. A name is used to identify request in the results file. So it is good to have a unique name for different requests. The “/products” is the URI GET request will open. See more about HTTP requests on Gatling HTTP Request page.

Once scenario and protocol have been defined those need to be assembled into a Simulation. Simulation class should extend Gatling’s io.gatling.core.Simulation class. This gives access to setUp() method which is configuring the simulation. setUp method takes a scenario with injected users in it scn.inject(atOnceUsers(1)). In this case, one used is injected at simulation start. There are different inject patterns that can be used. More about simulations setup can be found in Gatling Simulation setup page.

PersonSimulation explained

Below is recorded code for person simulation:

package com.automationrhapsody.gatling.simulations.original

import io.gatling.core.Predef._
import io.gatling.http.Predef._

class PersonSimulation extends Simulation {

	val httpProtocol = http
		.baseURL("http://localhost:9000")
		.inferHtmlResources()

	val headers_0 = Map(
		"Accept" -> "text/html,application/xhtml+xml,application/xml",
		"Upgrade-Insecure-Requests" -> "1")

	val headers_1 = Map(
		"Origin" -> "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop",
		"Postman-Token" -> "9577054e-c4a3-117f-74ab-e84a2be473e0")

	val headers_2 = Map(
		"Origin" -> "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop",
		"Postman-Token" -> "639b36ea-aff3-1b85-618e-c696734afc6e")

	val uri1 = "http://localhost:9000/person"

	val scn = scenario("RecordedSimulation")
		.exec(http("request_0")
			.get("/person/all")
			.headers(headers_0))
		.pause(9)
		.exec(http("request_1")
			.post("/person/save")
			.headers(headers_1)
			.body(RawFileBody("RecordedSimulation_0001_request.txt")))
		.pause(3)
		.exec(http("request_2")
			.post("/person/save")
			.headers(headers_2)
			.body(RawFileBody("RecordedSimulation_0002_request.txt")))

	setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

In short, scenario is following:

  • Invoke /person/all REST service on http://localhost:9000 URL.
  • Wait 9 seconds.
  • Save person by POST request to /person/save and RecordedSimulation_0001_request.txt JSON body.
  • Wait 3 seconds.
  • Save again person by POST request to /person/save and RecordedSimulation_0002_request.txt JSON body.

Most of the code here is similar to the one in PersonSimulation, so will not go over it again. Difference is the http(“request_1”) .post(“/person/save”) .headers(headers_1) .body(RawFileBody(“RecordedSimulation_0001_request.txt”)) code. It creates HTTP request with name “request_1”. Here request is POST to “/person/save” URI. POST data is put in the body by body() method, it loads “RecordedSimulation_0001_request.txt” file by reading it directly with RawFileBody() method. In many cases this is not convenient at all, it should be possible to parameterize the request and fill it with test data. This is done with ELFileBody() method.

If you have paid attention to all the readings here and to GitHub project you may have noticed that Gatling Maven plugin defaults say: <bodiesFolder>src/test/resources/bodies</bodiesFolder>, but the request is actually in src/test/resources folder of the project.

Conclusion

Recording scenario is a good way to get started but those needs to be refactored for efficiency and better maintenance. In order to be able to modify the first step is to understand what has been recorded. Once recordings are done those will be incorporated into Maven build. This is shown in Performance testing with Gatling – integration with Maven and Performance testing with Gatling – advanced usage posts.

Related Posts

Read more...

Performance testing with Gatling – Scala fundamentals

Last Updated on by

Post summary: Tutorial that covers basic Scala functionalities that may be needed during Gatling automation.

Current post is part of Performance testing with Gatling series in which Gatling performance testing tool is explained in details.

You need to have basic Java knowledge as this tutorial in most of the cases compares Scala with Java.

General concepts

In order to use Scala, its compiler needs to be installed from Scala home page. Scala source code is compiled to Java Byte Code and is run on Java Virtual Machine.

Scala is an object-oriented functional programming language. Everything in Scala is an object. There are no primitive types as in Java, those are represented by objects. Even functions are also objects.

There is very good interoperability with Java and Java code can be used inside Scala. Java object from Java classes can be created, also static methods in Java classes can be called.

Syntax

Scala syntax is pretty much the same as in Java. Although Scala is statically typed (checks are done at compile time) in most of the cases you do not need to explicitly specify variable type, Scala compiler knows it.

Scala is case-sensitive, class names are CamelCase with the first letter being capital, method names are also camel case starting with a lower case letter.

Semicolon “;” is not mandatory at the end of the line is it has only one expression. In case of more expressions on one line then is mandatory to separate each expression with a semicolon.

Classes and packages

The class file name should be with the same name as the class itself.

Import all classes from a package is done with an underscore:

import io.gatling.core.Predef._

Import of several classes from a package is done with curly brackets:

import org.joda.time.format.{DateTimeFormat, DateTimeFormatter}

Data types

As stated above everything is Scala is an object. There are no primitive types. Those are represented by objects:

  • Byte – 8 bit signed value. Range from -128 to 127
  • Short – 16 bit signed value. Range -32768 to 32767
  • Int – 32 bit signed value. Range -2147483648 to 2147483647
  • Long – 64 bit signed value. -9223372036854775808 to 9223372036854775807
  • Float – 32 bit IEEE 754 single-precision float
  • Double – 64 bit IEEE 754 double-precision float
  • Char – 16 bit unsigned Unicode character. Range from U+0000 to U+FFFF
  • String – a sequence of Chars
  • Boolean – either the literal true or the literal false
  • Unit – corresponds to no value
  • Null – null or empty reference
  • Nothing – the subtype of every other type; includes no values
  • Any – the supertype of any type; any object is of type Any
  • AnyRef – the supertype of any reference type

Strings and Arrays

Strings and arrays are very similar to Java ones. In Scala is possible to define multi-line string literal. This is done with three quotes: “””This is text

split into two lines”””

Variables

Variable declaration is done with the following format:

val or val VariableName : DataType [= Initial Value]

Example is: var answer : Int = 42. This is the full declaration. In most cases you do not need to provide data type, so above can be shortened to: var answer = 42.

With var you define a variable that is going to change its value. On the other hand, val keyword is used for defining variables that will never change, constants, like final in Java.

Access modifiers

Access modifiers are public, private and protected. There is no explicit keyword public though. If no modifier is used then access level is considered public.

private and protected can be defined for a specific scope.

private[com.automationrhpasody.gatling]

In the example above, field, method or class that this modifier is applied to is considered private for all the world, except for classes in package com.automationrhpasody.gatling. The scope can be a single class or singleton object (more about those will follow).

Operators

Operators are very similar to Java. Those are:

  • Arithmetic Operators (+, -, *, /, %)
  • Relational Operators (==, !=, >, <, >=, <=)
  • Logical Operators (&&, |, !)
  • Bitwise Operators (&, |, ^, ~, <<, >>, >>>)
  • Assignment Operators (=, +=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=)

Unlike Java there are no ++ and — operators. This is because everything in Scala is object and default data types are immutable objects. So modifications on current object are not possible, new object is created instead, so it is not possible to have ++ and — operations.

Conditional statements

A conditional statement is if/else same as in Java:

if (condition) {
	statement
}

if (condition) {
	statement1
} else {
	statement2
}

if (condition1) {
	statement1
} else if (condition2) {
	statement2
} else {
	statement3
}

Loop statements

Loop statements are for, while, do/while. For statement is very specific, the easiest way to use it is with Ranges:

for (i <- 1 until 10) {
	println("i=" + i)
}

The code above will print values from 1 to 9. Operator <- is called generator as it generating the individual values from the range. 1 until 10 is the actual range. This is the number from 1 to 9, 10 is not included. If you want the last value included use to: 1 to 10.

It is possible to put some conditional statement in the for loop with if clause:

for (i <- 1 until 10; if i >= 3 && i <= 6) {
	println("i=" + i)
}

The code above will print values 3, 4, 5 and 6. Only those are >= 3 and <= 6 from the whole range.

Nested for loops are pretty easy in Scala and not that apparent, so it is easy to get into performance issues:

for (i <- 1 until 5; j <- 1 to 3) {
	println("i=" + i)
	println("j=" + j)
}

The code above is equal to Java code:

for (int i = 0; i < 5; i++) {
	for (int j = 0; j <= 3; j++) {
		System.out.println("i=" + i);
		System.out.println("j=" + j);
	}
}

For loop can also be used with collections:

val daysOfWeek = List("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
for (str <- daysOfWeek) {
	println("day=" + str);
}

You can also iterate Java collections, but first, they need to be converted by classes in scala.collection.JavaConversions package. Below is example iterating Java’s System Properties:

import scala.collection.JavaConversions._

for (key <- System.getProperties.keySet().iterator.toIterator) {
	println("Key=" + key + ", Value=" + System.getProperty(key.toString))
}

Loop statements while and do/while as in Java:

while (condition) {
	statement
}

do {
	statement
} while (condition)

Methods and functions

Scala has both functions and methods. This is a group of statements and conditions that perform a task and can return a result. Difference between those is that method can be defined only inside a class. A function can be defined anywhere in the code. This is an object that can be assigned to a variable. Functions and method names in Scala can contain special characters such as +, -, &, ~, etc.

A function definition is:

def functionName ([list of parameters]) : [return type] = {
	function body
	[return] [expr]
}

Data in [] are optional. The return type is any valid Scala type. If nothing is to be returned then Unit is returned, this is as void methods in Java. As you can notice even return statement, in the end, is not mandatory and can be omitted, then the function will return the statement that comes last.

def sum(a: Int, b: Int): Int = {
	a + b
}

Function/method above sums two Integer values and returns the result of the sum.

A function is called the same way as in Java:

[object.]functionName([list of parameters])

Scala is function based language, so there are many many things that can be done with functions. More you can read on Scala functions in Tutorialspoint.

Collections

A collection is a group of objects. Scala API has a rich set of collections. By default collections in Scala are immutable. This means current collection object cannot be changed, if you do some change on a collection this results in the creation of new collection with desired changes applied in it. This is made for thread safety. There are also mutable collections. They can be found in scala.collection.mutable package.

  • Lists – lists are very similar to arrays, you have elements in sequential order that can be accessed.
  • Sets – collection of unique elements, there are no duplicates.
  • Maps – a collection of key/value pairs. Values are retrieved based on their unique key.
  • Tuples – combines several elements together so they can be considered one object.
  • Iterators – not actually a collection, but a way to access elements from a collection one by one.

More about Scala collections can be found in Scala collections in Tutorialspoint.

Classes, objects

A class is a blueprint for objects. A class is like a cookie cutter. Objects are the cookies. Objects are created with keyword new. A class name is a class constructor taking different arguments. You can pass parameters to it. A class can have several constructors. The restriction is that each constructor should call on its first line some other constructor. Eventually, the main constructor in the class name will get called. Classes can be extended similarly to Java.

class Car(isStarted: Boolean) {
	var _isStarted: Boolean = isStarted
	var _speed: Int = 0

	def this() = this(true)

	def start(): Unit = {
		_isStarted = true
		println("Car is started: " + _isStarted)
	}

	def drive(speed: Int): Unit = {
		_speed = speed
		if (_isStarted) {
			println("Car moving with speed: " + _speed)
		} else {
			println("Car is not started!")
		}
	}
}

Singleton objects

In Scala, there is no definition of static methods or fields. In order to accomplish similar functionality, you can use singleton objects, classes with only one instance defined with object keyword.

object UseThatCar {
	def main(args: Array[String]) {
		val car1 = new Car(false)
		car1.drive(50)

		val car2 = new Car()
		car2.drive(45)
	}
}

Resources

I do not tend to be Scala expert. I know little but enough to make Gatling performance testing. So I have used other network resources to build this post. A very good tutorial is Scala in Tutorialspoint. Another interesting post is The 10 Most Annoying Things Coming Back to Java After Some Days of Scala.

Conclusion

People that have worked with Scala really love it. It is made for performance and scalability. Because everything is object and default types are immutable objects Scala is much more thread safe than Java. Functions can be defined anywhere and do everything you want, so you can do almost everything with just functions if you like. Although it is that good Scala is compiled to Byte Code and run on Java Virtual Machine, so limitations applicable to JVM are applicable to Scala executable code. No matter if you like it or not, you have to learn basics of Scala as Gatling is written in Scala and in order to do performance testing with it you have to know Scala fundamentals.

Related Posts

Read more...