Performance testing is a non-functional software testing technique that determines how the stability, speed, scalability, and responsiveness of an application holds up under a given workload.

Steps

  • Figure out crucial user journey

  • Estimate load (typical hour, peak hour, off-hour)

  • Simulate user journey

  • Analysis performance report (max, min, mean, p95, 99p response time, fail rate, fail reason)

  • Come up with actions, improvements

How to write performance test using Gatling

Scenario

To represent users’ behaviors

        val onBoarding = scenario("On boarding")
            .feed(onBoardingCustomerFeeder)
            .exec { session -> session.set("timestamp", "${MILLISECONDS.toMicros(System.currentTimeMillis())}") }
            .exec(CustomerManager.getConsents)
            .exec(CustomerManager.customerProspect)
            .exec(IamManager.createCredentialId)
            .exec(Vida.fetchChallenge)
            .exec(Vida.registerPublicKey)
            .exec(CustomerManager.requestAaiSdkLicense)
            .exec(CustomerManager.getBreadcrumbs)
            .exec(CustomerManager.saveCustomerPreferences)
            .exec(CustomerManager.fetchCountries)
            .exec(CustomerManager.signUp)
            .exec { session -> session.set("otp", retrieveOtp(session.getString("countryCode"), session.getString("phone"))) }
            .exec(CustomerManager.verifyOtp)

Simulation

A simulation is a description of the load test. It describes how, possibly several, user populations will run: which scenario they will execute and how new virtual users will be injected.

class OnBoardingPerfTest : Simulation() {
    init {
        setUp(onBoarding.injectOpen(rampUsers(10).during(5)))
            .protocols(httpProtocol)
    }
}

Session

Each virtual user is backed by a Session. Those Sessions are the actual messages that go down the scenario workflow. A Session is basically a state placeholder, where testers can inject or capture and store data.

val onBoarding = scenario("On boarding")
            .feed(onBoardingCustomerFeeder)
            ......
            .exec(CustomerManager.signUp)
            // retrieve otp and store it in session
            .exec { session -> session.set("otp", retrieveOtp(session.getString("countryCode"), session.getString("phone"))) }
            .exec(CustomerManager.verifyOtp)
object IamManager {
    val createCredentialId = http("Create credential id")
        .post("iam-manager/credential")
        .body(StringBody("""{"customerId": "#{customerId}"}"""))
        .check(
            status().shouldBe(201),
            jsonPath("$.state").shouldBe("IN_PROGRESS"),
            //Store credential id in session which will be used later
            jsonPath("$.credentialId").exists().saveAs("credentialId"),
        )
}

Feeder

Feeders are a convenient API for testers to inject data from an external source into the virtual users’ sessions.

object Feeders {
    private const val phoneChars = "0123456789"
    private val toneOfVoice = listOf("FORMAL", "FRIENDLY")
    private val languages = listOf("TAGLISH", "ENGLISH")

    val onBoardingCustomerFeeder = generateSequence {
        val (silentPrivateKey, silentPublicKey) = CryptoUtil.generateKeyPairBase64Encoded()
        val (presencePrivateKey, presencePublicKey) = CryptoUtil.generateKeyPairBase64Encoded()
        mapOf(
            "email" to "perf-test-${UUID.randomUUID()}@advancegroup.com",
            "phone" to (1..10).map { phoneChars.random() }.joinToString(""),
            "silentPrivateKey" to silentPrivateKey,
            "silentPublicKey" to silentPublicKey,
            "presencePrivateKey" to presencePrivateKey,
            "presencePublicKey" to presencePublicKey,
            "preferenceToneOfVoice" to toneOfVoice.random(),
            "preferenceLanguage" to languages.random()
        )
    }.iterator()
}

Check

A check is a response processor that captures some part of it and verifies that it meets some given condition(s).

    val customerProspect = http("Customer prospect")
        .post("customer-manager/v1/customers/prospect")
        .body(StringBody("""{"consentIds":#{consentIds}}"""))
        .header("X-Idempotency-Key") { UUID.randomUUID().toString() }
        .check(
            // Check http status code
            status().shouldBe(200),  
            // Check response body
            jsonPath("$.id").exists().saveAs("customerId"),
            jsonPath("$.status").shouldBe("PROSPECT")
        )

Assertion

Assertions are used to define acceptance criteria on Gatling statistics (e.g. 99th percentile response time) that would make Gatling fail and return an error status code for the test as a whole.

class OnBoardingPerfTest : Simulation() {
    init {
        setUp(onBoarding.injectOpen(rampUsers(5).during(5)))
            .protocols(httpProtocol)
            .assertions(
                global().responseTime().percentile(99.0).lte(2000),
                global().responseTime().max().lt(5000),
                global().failedRequests().count().shouldBe(0)
            )
    }
}

Report

By default, reports are automatically generated at the end of a simulation. They consist of HTML files.

Demo

https://github.com/SafiBank/PerfTest

References

https://gatling.io/docs/gatling/reference/current/core/concepts/