How to generate multivalue map with random values and inject into request body using a feeder in Gatling - scala

I'm writing a load test for an api and want to create a feeder which generates random values to inject into the body of the POST request. I initially tried copying the random email example from the documentation and adding additional fields to the generated map, but when that didn't work I went down to a single field, basically copying the documentation; however, even this doesn't work for some reason. There are a bunch of solutions on here that use this syntax as well, but something about the way I'm doing is causing the fields I try to inject into the body to be null when the request is made.
Current Code:
val userFeeder: Iterator[Map[String, Unit]] =
Iterator.continually(Map("userName" -> ("user_" + Random.alphanumeric.take(15).mkString)))
var scn: ScenarioBuilder = scenario("GENERATE USER")
.feed(userFeeder)
.exec(
http("CREATE USER")
.post(userBaseUrl)
.headers(userHeaders)
.body(StringBody("userName: ${userName}")))
setUp(
scn.inject(atOnceUsers(1))
)
Ideally I'd like to be able to expand the feeder to include multiple values, i.e.
val userFeeder: Iterator[Map[String, Unit]] =
Iterator.continually(Map("userName" -> ("user_" + Random.alphanumeric.take(15).mkString),
"userEmail" -> (Random.alphanumeric.take(15).mkString) + "#random.edu"),
"address" -> Random.alphanumeric.take(15).mkString)))
and so on, but I'm a little stumped as to why my current code doesn't even work, as it seems to follow the documentation example pretty faithfully. The values are always null in my requests despite trying a few different strategies.
Log output
body:StringChunksRequestBody{contentType='application/json', charset=UTF-8, content=userName: ()}

Figured it out. Turns out that even though Feeder is a wrapper for Iterator, the proper way to do what I want is to declare it like this:
val userFeeder: Feeder[Any] =
Iterator.continually(Map("userName" -> ("user_" + Random.alphanumeric.take(15).mkString),
"userEmail" -> (Random.alphanumeric.take(15).mkString) + "#random.edu"),
"address" -> Random.alphanumeric.take(15).mkString)))

Related

How to set different protocol for each user (threads running) in gatling

I am trying to create a performance test suite where i have array of ID's that will be picked in random. For each ID , there is a designated auth_token assigned .
In the protocol , if i pass the method where the random ID's would take , it always sets it to that particular ID for the entire operation .
I am expecting something like , i am defining 10 virtual users and for each user the protocol should change the ID and continue the scenario execution .
Currently Gatling is setting the protocol at the first and uses the same protocol for all the 10 users.
id = random.generate //generate random id
authHeader = Method(id);
def method (id:String) : String{
if(id=="id1")
return token1
else if(id=="id2")
return token2
""
}
val httpProtocol = http.baseUrl(baseURI)
.acceptHeader(header)
.authorizationHeader(authHeader)
.contentTypeHeader(contentType)
.userAgentHeader(agentHeader)
val scn1: ScenarioBuilder = scenario("name")
.exec(http("scenario1")
.post(device_context)
.body(payload)
.check(status.is(202)))
setUp(scn1.inject(atOnceUsers(2)).protocols(httpProtocol))```
In the above code i need the suite to run for 2 different id.
To make this work, you're going to have to use a session variable to hold your authHeader. The DSL methods define builders that are only executed once - which explains what you're seeing.
The best way to do this would be to construct a feeder to hold your ids and auth tokens (I'm assuming you're not using the id anywhere else)
So define a feeder that maps a key (just a string) to a map that contains the id and auth token
//a list of strings to hold the auth tokens
private val ids = List(Map("id" -> "id1", "auth" -> "token1"), Map("id" -> "id2", "auth" -> "token2"), ...)
//the feeder to put a random token into the session
private val idFeeder = Iterator.continually(Map("id" -> Random.shuffle(ids).head))
now when you call .feed on idFeeder you get a random Map that has keys for "id" and "auth" in the session variable "id"
so update your scenario to use the feeder and set an Authorization header (and remove .authorizationHeader from your protocol definition)
val scn1: ScenarioBuilder = scenario("name")
.feed(idFeeder)
.exec(http("scenario1")
.header("Authorization", "${id.auth})"
.post(device_context)
.body(payload)
.check(status.is(202)))
in your body you can access the user id string with "${id.id}"
alternatively you could update your protocol definition to have the ${id} reference, but I find it nicer to have the feeder value used in the same block as the call to .feed

Not able to extract values in Gatling

I have written 2 sets of Gatling code. My usecase in example 1 is make a post call. extract a value. make a get call and use the extracted value in header.
My usecase in example 2 is make a post call, extract a value, make a get call use the extracted value in cookie.
Example 1
val login = http("Login")
.post("login")
.body(StringBody("""{"username": "foo", "password": "bar"}""")).asJSON
.check(status.is(200), jsonPath("$..response.id").ofType[String].saveAs("id"))
val get = http("get")
.get("foo")
.header("token", "$id")
.check(status.is(200), jsonPath("$..response").exists)
var id = ""
val scn = scenario("scenario")
.exec(login)
.exec(session => {
id = session("id").as[String].trim
println("+++++++" + id)
session}
)
.pause(3)
.exec(get)
When I run this code I see that the print statement above prints the correct ID. The server throws 403 on the get call because the ID is not being set correctly. If I take any of the printed values and then remote $id and replace it with that. the test runs correctly.
So how do I access the saved variable?
Example 2
val login = http("Login")
.post("login")
.body(StringBody("""{"username": "foo", "password": "bar"}""")).asJSON
.check(status.is(200), jsonPath("$..response.id").ofType[String].saveAs("id"))
val get = http("get")
.get("foo")
.check(status.is(200), jsonPath("$..response").exists)
val testCookie = scenario("test-cookie")
.exec(login)
.pause(3)
.exec(addCookie(Cookie("foo_cookie", "$id")))
.exec(get)
Here also the value is not extracted successfully and I get a 403 when I run it because the "foo_cookie" was not set correctly and the server will throw a 403 if the cookie is not found. (in postman I can make the same call work by specifying the cookie correctly)
I was able to resolve the issue. The problem was this line
.header("token", s"$id")
In Scala $id and ${id} are same but it appears that in Gatling they are not.
when I replaced my code to
.header("token", "${id}")
note that there is no "s" behind the string.
it worked!

Avoid query parameters duplication when generating a MAC for requests in gatling

I am using gatling (2.1.7) in order to stress test an API.
For of all I have to request a transaction id and shared secret in order to authenticate all subsequent calls.
scenario("API").exec(http("authorize")
.post("/api/v1/xxx/authorize")
.formParam("client_key", "a_very_strong_key")
.check(jsonPath("$.response.txId").saveAs("id"))
.check(jsonPath("$.response.txSecret").saveAs("secret")))
All other calls must contain a query parameter signature which is a mac of the other request parameters.
I wrote this piece of code to do that
scenario("API").exec(http("call")
.get("/api/v1/call")
.queryParam("id", "${id}")
.queryParam("param1", "aaaaaa")
.queryParam("param2", "bbbbbb")
.queryParam("signature", session => sign(session, Map(
"id" -> session("id").as[String],
"param1" -> "aaaaaa",
"param2" -> "bbbbbb"))))
/* ... */
def sign(session: Session, params: Map[String, String]) : String = {
val str = canonicalize(params)
format_mac(session("secret").as[String], str)
}
However I have to duplicate all query parameter names and values in the sign method call and it is clearly a bad practice. It is possible to avoid that ?
You want to sign a request, so use a SignatureCalculator.

PlayFramework Testing: Uploading File in Fake Request Errors

I want to test my method, which requires uploading a file. It is initialized like this:
val tempFile = TemporaryFile(new java.io.File("/home/ophelia/Desktop/students"))
val part = FilePart[TemporaryFile](
key = "students",
filename = "students",
contentType = Some("text/plain"),
ref = tempFile)
val files = Seq[FilePart[TemporaryFile]](part)
val formData = MultipartFormData(
dataParts = Map(),
files = Seq(part),
badParts = Seq(),
missingFileParts = Seq())
I pass it into the FakeRequest:
val result = route(
FakeRequest(POST, "/api/courses/"+"4f3c4ec9-46bf-4a05-a0b2-886c2040f2f6"+"/import" )
.withHeaders("Authorization" -> ("Session " + testSessionA.id.string))
.withMultipartFormDataBody(formData)
)
But when I run the test I get the following error:
Cannot write an instance of play.api.mvc.AnyContentAsMultipartFormData to HTTP response. Try to define a Writeable[play.api.mvc.AnyContentAsMultipartFormData]
What am I doing wrong and how to fix it? I looked on the internet, I didnt find any useful way to understand and resolve this problem.
It's important to remember that http requests are entirely text. route() takes an implicit Writeable to convert the body type of the provided request into text. Without the right Writeable, there is no way to know how to turn MultipartFormData into a request body.
There doesn't seem to be a Writeable for MultipartFormData, but you can provide your own. jroper has a great Writeable you could use for reference. (EDIT: That code is buggy, here's a working Writeable for AnyContentAsMultipartFormData)
Once you have your Writeable, you will need to make it accessible to your call to route(). Bear in mind, you currently have a FakeRequest[AnyContentAsMultipartFormData], not a FakeRequest[MultipartFormData]. You can either convert your request first:
val request = FakeRequest(POST,
"/api/courses/"+"4f3c4ec9-46bf-4a05-a0b2-886c2040f2f6"+"/import" )
.withHeaders("Authorization" -> ("Session "))
.withMultipartFormDataBody(formData)
route(request.map(_.mdf).asInstanceOf[FakeRequest[MultipartFormData[TemporaryFile]]])
or make your Writeable a Writeable[AnyContentAsMultipartFormData].
route for a given Request[T] requires an implicit parameter of type Writeable[T] that knows how to serialize the request body, because it will actually call the controller action just like an actual web request would, by pushing bytes onto it.
The problem is that there is no Writeable[MultipartFormData] predefined (you can see which are in play.api.test.Writeables).
This means you basically have two options:
write your own Writeable that serializes a MultipartFormData into bytes
Skip the routing part and call the action directly instead, like in the accepted answer in Play Framework Testing using MultipartFormData in a FakeRequest. This way of testing actions takes a shortcut and does not actually serialize and deserialize the request.
IMHO the first option is way too much pain for the gain, but if you go down that road, maybe contribute it to play when you succeed.
One of the possible solutions is to use wsUrl. For example
"File uploading action" should {
"upload sent file and result in ID" in {
val file = Paths.get(getClass.getResource("/1.txt").toURI)
val action = wsUrl("/upload").post(Source.single(FilePart("file", "hello.txt", Option("text/plain"), FileIO.fromPath(file))))
val res = Await.result(action, timeout)
res.status mustBe OK
res.body contains "123"
}
}

Gatling 2 dynamic queryParam on each request

I am trying to run a load test using Gatling 2. I need to generate one of the query parameters dynamically on each request.
My scenario is defined like this:
val scn = scenario("Load Test Scenario")
.exec(
http("Test API")
.post(url)
.body(StringBody("Some XML"))
.queryParam("x", DigestUtils.md5Hex(generateX().getBytes("UTF-8")))
)
def generateX() : String = {
// generate random string and return
}
This only calls generateX once and uses the result in each request. Is there anyway to have the generateX call on every request?
You have to pass a function, not a value. See Gatling documentation about Expression.
Here, you can just discard the session input parameter as you don't use it, so you can simply write:
.queryParam("x", _ => DigestUtils.md5Hex(generateX().getBytes("UTF-8")))