In the previous blog post we created a realistic Virtual User that browses the store without buying anything.
On the contrary, here we are going to simulate the behavior of a user that connects to the web store, searches for items, adds some to his cart and proceeds to the checkout.
Then we will combine both Virtual Users to simulate a diverse load on the PetStore.
A header field (Content-Type) in the POST request usually indicates the message body's Media Type.
Web applications such as our PetStore typically send POST requests via the submission of an HTML form.
The content type of the request varies depending on what is sent or how it is sent:
application/x-www-form-urlencoded: keys and values are encoded in key-value pairs separated by '&', with a '=' between the key and the value. All non-alphanumeric characters are percent encoded.
multipart/form-data: each value is sent as a body part, boundaries separating each part. This format allows to send binary data (i.e. file upload), while the x-www-form-urlencoded format prevents this because of the encoding.
Modern web application like SPAs made with Angular, React or Vue send POST requests with an XMLHttpRequest instead of a form.
In this case the content type can be anything.
For instance, it's usually application/json when sending a JSON object to the server.
POST vs PUT:
The difference between PUT and POST is idempotence:
Calling a PUT request once or several times successively has the same effect,
Successive identical POST request may have side effects, like adding an item to a cart several times.
Gatling's DSL addresses all the cases previously mentioned.
It automatically sets the Content-Type header for you if you do not specify one:
It will use application/x-www-form-urlencoded except if there are body parts, in which case it will set it to multipart/form-data.
To send a URL encoded form, use the syntax: http("URL Ecnoded POST").post("request-url").formParam("key", "value"),
To post a multipart request, for instance by uploading a file: http("Multipart POST").post("request-url").formParam("key", "value").formUpload("key2", "fileToUpload.txt"),
To simulate sending a JSON body to a REST API: http("JSON Body").post("request-url").header("Content-Type", "application/json").body(RawFileBody("body.json")).asJson.
The file body.json must be present in the project resources. You can replace RawFileBody by ElFileBody if you need to inject session variables into the body.
Let's try to apply this to our e-commerce and create a Login request.
Le login process is divided in several steps.
A first GET request fetches the Login HTML Form:
The user types his credentials and clicks on the Login button.
A POST request (application/x-www-form-urlencoded) is send to the server.
It answers with a 302 HTTP response code and the header location: /actions/Catalog.action to redirect the user to the homepage.
Here is a Chrome console view of the Login request:
To simulate the connexion of different users, we use a CSV Feeder
val credentials = csv("four/credentials.csv").random
Here we use the keyword .post combined with .formParam to send a URL encoded form with the same parameters that we saw in the Chrome console screenshot.
We also check if the message "Welcome ABC!" is present in the response.
The scenario definition uses the feeder to inject the user credentials and chains the two requests:
val scn = scenario("PetStoreSimulation")
.exec(signonFormRequest)
.feed(credentials)
.exec(loginRequest)
The complete script for this chapter is available:
So we have two virtual users, one from the previous tutorial that browses the shop, and one we just created.
Our goal is to start both VUs at the same time, in the same scenario. How to do this without duplicating the scripts?
The idea is to split our Virtual Users in smaller chunks, for example the login operation or the search.
Then we use these test parts in a bigger simulation.
A benefit, in addition to producing more readable scripts, is that if the tested application changes you only have to update one script chunk.
Let's start with the Visitor behavior.
We get the Simulation from the previous blog post and transform it into a requests and scenario "module".
It's straightforward since we already declared a var for each request and scenario. We just need to drop the extends Simulation and change the class into an objet:
The final step is to import these two objects (request and scenario modules) into a unique Simulation class:
package com.octoperf.tutorials.four
import scala.concurrent.duration._
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import com.octoperf.tutorials.four.PetStoreSimulationBuyer._
import com.octoperf.tutorials.four.PetStoreSimulationVisitor._
class PetStoreSimulationModular extends Simulation {
val httpProtocol = http
.baseUrl("https://petstore.octoperf.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 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
.inferHtmlResources(BlackList(), WhiteList("https://petstore.octoperf.com/.*"))
setUp(PetStoreSimulationBuyer.scn.inject(constantConcurrentUsers(2) during(2 minutes)),PetStoreSimulationVisitor.scn.inject(constantConcurrentUsers(10) during(2 minutes))).protocols(httpProtocol)
}
The modules are imported with the syntax import com.octoperf.tutorials.four.PetStoreSimulationBuyer._ and import com.octoperf.tutorials.four.PetStoreSimulationVisitor._.
We will simulate 2 concurrent buyers and 10 concurrent visitors during 2 minutes.
This is done by setting up the load test using their respective scenario setUp(PetStoreSimulationBuyer.scn.inject(constantConcurrentUsers(2) during(2 minutes)),PetStoreSimulationVisitor.scn.inject(constantConcurrentUsers(10) during(2 minutes))).protocols(httpProtocol).
The complete scripts for this chapter are available:
Launching this test on two load injectors shows us that both virtual users are executed.
We only scratched the surface of what could be done in terms of modularization, for example:
An object PetStoreFeeders could be created with all feeders used to load test the JPetStore,
Another one could hold the set of requests used to login and logout,
A third one could contain the requests to search and browse random items,
etc.
In the examples above we only declared HTTP request variables (val searchRequest = http("Search ${term}") ...).
You can also declare variables that contain the successive execution of several requests:
val login = exec(signonFormRequest)
.pause(500 milliseconds)
.feed(credentials)
.exec(loginRequest)
.pause(500 milliseconds)
And use it in a scenario: val scn = scenario("Buyer").exec(login, search, checkout, logouut).