3 Commits

Author SHA1 Message Date
lq64 25ecc1e097 feat: NCS-73 Refine Gatlin tests to reflect ordinary user behaviour (#7)
Added realistic user behaviour
Smoke Test

  1 user, runs once.
  Executes the full user journey exactly once. The goal is a fast sanity check — if this passes, the system is up
  and the critical path (register, login, import, move, resign) works end-to-end. Run this before any heavier
  test.

  ---
  Load Test

  Ramps up to maxUsers (default 10) over rampDuration (default 60s).
  Simulates normal growing traffic. Users are added gradually, each completing the full journey. This reveals how
  the system behaves under typical production load and establishes a performance baseline (response times, error
  rates) to compare other tests against.

  ---
  Stress Test

  Starts at startUsers (default 2), adds usersIncrement (default 2) users every stepDuration (default 30s) for
  steps (default 2) steps, with ramps between.
  Deliberately pushes the system beyond normal capacity in controlled steps. Each step holds load steady before
  increasing, so you can pinpoint exactly at which user count the system starts degrading or failing — the
  breaking point.

  ---
  Endurance Test

  Holds concurrentUsers (default 3) constant for duration (default 300s).
  Keeps a steady number of users playing full games repeatedly over a long period. This is the most important test
   for detecting slow degradation — memory leaks, connection pool exhaustion, database lock buildup — things that
  only appear after sustained use rather than under peak load.

  ---
  Spike Test

  Baseline → sudden burst of spikeUsers (default 15) → 5s pause → back to baseline.
  Simulates an unexpected traffic surge (e.g. a news article or viral moment). Tests whether the system can absorb
   a sudden large wave of users registering and starting games simultaneously, and whether it recovers cleanly
  once the spike subsides.

---------

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #7
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
2026-05-05 19:51:12 +02:00
lq64 427ed920d0 feat: NCS-62, NCS-64 Endurance Test, Spike Test (#6)
Added two new types of tests

---------

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #6
2026-05-05 17:17:38 +02:00
lq64 99ea686ed5 feat: NCS-60 Stress Test (#3)
Added a stress test and configured the parameters to simulate a smaller load

---------

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #3
2026-05-05 17:09:42 +02:00
8 changed files with 163 additions and 22 deletions
+6
View File
@@ -23,6 +23,12 @@ tasks.withType(io.gatling.gradle.GatlingRunTask) {
"-Dhealthz.path=${findProperty('healthzPath') ?: '/health'}",
"-DmaxUsers=${findProperty('maxUsers') ?: '10'}",
"-DrampDuration=${findProperty('rampDuration') ?: '60'}",
"-DstartUsers=${findProperty('startUsers') ?: '2'}",
"-DusersIncrement=${findProperty('usersIncrement') ?: '2'}",
"-Dsteps=${findProperty('steps') ?: '2'}",
"-DstepDuration=${findProperty('stepDuration') ?: '30'}",
"-DconcurrentUsers=${findProperty('concurrentUsers') ?: '3'}",
"-Dduration=${findProperty('duration') ?: '300'}",
"-DbaselineUsers=${findProperty('baselineUsers') ?: '2'}",
"-DbaselineDuration=${findProperty('baselineDuration') ?: '20'}",
"-DspikeUsers=${findProperty('spikeUsers') ?: '15'}"
@@ -6,9 +6,9 @@ object BoardEndpoints {
val createGame: Endpoint = Endpoint(
name = "Create Game",
method = "POST",
path = "/api/board/game/",
expectedStatus = 201
method = "GET",
path = "/api/account/official-bots/",
// expectedStatus = 200
)
val all: List[Endpoint] = List(createGame)
@@ -0,0 +1,61 @@
package scenarios
import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._
import scala.concurrent.duration._
object ChessUserScenario {
private def makeMove(uci: String) =
http(s"Move $uci")
.post(session => s"/api/board/game/${session("gameId").as[String]}/move/$uci")
.header("Authorization", "${jwt}")
.check(status.in(200, 201))
val play: ScenarioBuilder = scenario("Chess User Journey")
.exec(session => session.set("username", s"user_${System.currentTimeMillis()}_${session.userId}_${java.util.UUID.randomUUID().toString.take(8)}"))
.exec(
http("Register")
.post("/api/account")
.body(StringBody(session =>
s"""{"username":"${session("username").as[String]}","email":"${session("username").as[String]}@test.com","password":"Password123!"}"""
))
.check(status.is(200))
)
.exec(
http("Login")
.post("/api/account/login")
.body(StringBody(session =>
s"""{"username":"${session("username").as[String]}","password":"Password123!"}"""
))
.check(status.is(200))
.check(jsonPath("$.token").saveAs("jwt"))
)
.exec(
http("Import Game")
.post("/api/board/game/import/fen")
.header("Authorization", "${jwt}")
.body(StringBody(session => {
val username = session("username").as[String]
s"""{
| "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
| "white": {"id": "$username", "displayName": "$username"},
| "black": {"id": "opponent_${session.userId}", "displayName": "Opponent"},
| "timeControl": {"limitSeconds": 300, "incrementSeconds": 3}
|}""".stripMargin
}))
.check(status.in(200, 201))
.check(jsonPath("$.gameId").saveAs("gameId"))
)
.exec(makeMove("e2e4"))
.exec(makeMove("e7e5"))
.exec(makeMove("g1f3"))
.exec(
http("Resign")
.post(session => s"/api/board/game/${session("gameId").as[String]}/resign")
.header("Authorization", "${jwt}")
.check(status.in(200, 201, 204))
)
}
@@ -0,0 +1,26 @@
package simulations
import base.BaseSimulation
import scenarios.ChessUserScenario
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class EnduranceTestSimulation extends BaseSimulation {
private val concurrentUsers = sys.props.getOrElse("concurrentUsers", "3").toInt
private val duration = sys.props.getOrElse("duration", "300").toInt
override protected val httpProtocol = http
.baseUrl(baseUrl)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
setUp(
ChessUserScenario.play
.inject(
constantConcurrentUsers(concurrentUsers).during(duration.seconds)
)
).protocols(httpProtocol)
}
@@ -1,20 +1,25 @@
package simulations
import base.BaseSimulation
import endpoints.BoardEndpoints
import scenarios.ChessUserScenario
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class LoadTestSimulation extends BaseSimulation {
private val maxUsers = sys.props.getOrElse("maxUsers", "10").toInt
private val rampDuration = sys.props.getOrElse("rampDuration", "60").toInt
private val maxUsers = sys.props.getOrElse("maxUsers", "5").toInt
private val rampDuration = sys.props.getOrElse("rampDuration", "60").toInt
// Each virtual user authenticates individually, so no global Bearer token
override protected val httpProtocol = http
.baseUrl(baseUrl)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
setUp(
BoardEndpoints.all.map { endpoint =>
scenarioFromEndpoint(endpoint)
.inject(rampUsers(maxUsers).during(rampDuration.seconds))
}: _*
ChessUserScenario.play
.inject(rampUsers(maxUsers).during(rampDuration.seconds))
).protocols(httpProtocol)
}
@@ -1,13 +1,19 @@
package simulations
import base.BaseSimulation
import endpoints.BoardEndpoints
import scenarios.ChessUserScenario
import io.gatling.core.Predef._
import io.gatling.http.Predef._
class SmokeTestSimulation extends BaseSimulation {
override protected val httpProtocol = http
.baseUrl(baseUrl)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
setUp(
scenarioFromEndpoint(BoardEndpoints.createGame)
ChessUserScenario.play
.inject(atOnceUsers(1))
).protocols(httpProtocol)
}
@@ -1,8 +1,9 @@
package simulations
import base.BaseSimulation
import endpoints.BoardEndpoints
import scenarios.ChessUserScenario
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
@@ -12,15 +13,18 @@ class SpikeTestSimulation extends BaseSimulation {
private val baselineDuration = sys.props.getOrElse("baselineDuration", "20").toInt
private val spikeUsers = sys.props.getOrElse("spikeUsers", "15").toInt
override protected val httpProtocol = http
.baseUrl(baseUrl)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
setUp(
BoardEndpoints.all.map { endpoint =>
scenarioFromEndpoint(endpoint)
.inject(
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds),
atOnceUsers(spikeUsers),
nothingFor(5.seconds),
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds)
)
}: _*
ChessUserScenario.play
.inject(
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds),
atOnceUsers(spikeUsers),
nothingFor(5.seconds),
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds)
)
).protocols(httpProtocol)
}
@@ -0,0 +1,33 @@
package simulations
import base.BaseSimulation
import scenarios.ChessUserScenario
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class StressTestSimulation extends BaseSimulation {
private val startUsers = sys.props.getOrElse("startUsers", "2").toInt
private val usersIncrement = sys.props.getOrElse("usersIncrement", "2").toInt
private val steps = sys.props.getOrElse("steps", "2").toInt
private val stepDuration = sys.props.getOrElse("stepDuration", "30").toInt
private val rampDuration = sys.props.getOrElse("rampDuration", "10").toInt
override protected val httpProtocol = http
.baseUrl(baseUrl)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
setUp(
ChessUserScenario.play
.inject(
incrementConcurrentUsers(usersIncrement)
.times(steps)
.eachLevelLasting(stepDuration.seconds)
.separatedByRampsLasting(rampDuration.seconds)
.startingFrom(startUsers)
)
).protocols(httpProtocol)
}