Skip to main content

Rethinking Gatling: A Scala CLI and Containerisation Approach to Performance Testing

Picture of Michał Wiącek, Software Engineer

Michał Wiącek

Software Engineer
Oct 27, 2025|20 min read
logos_gatling_Scala_cli_docker
1final case class SimulationConfig(
2 simulationClass: Class[? <: Simulation],
3 results: Path,
4 sleepTime: FiniteDuration,
5 users: Int
6) {
7 def simulationClassName: String = simulationClass.getName
8}
9object SimulationConfig {
10 given pathEncoder: Encoder[Path] = Encoder.encodeString.contramap(_.toString)
11 given pathDecoder: Decoder[Path] = Decoder.decodeString.map(Paths.get(_))
12 given durationDecoder: Decoder[FiniteDuration] = Decoder.decodeString.emap { durationStr =>
13 Try(Duration(durationStr)).toEither.left
14 .map(ex => s"Failed to parse duration '$durationStr': ${ex.getMessage}")
15 .flatMap(duration =>
16 Either.cond(
17 duration.isFinite,
18 duration.asInstanceOf[FiniteDuration],
19 s"Duration '$durationStr' is not finite"
20 )
21 )
22 }
23 given simulationClassEncoder: Encoder[Class[? <: Simulation]] =
24 Encoder.encodeString.contramap(_.getName)
25 given simulationClassDecoder: Decoder[Class[? <: Simulation]] =
26 Decoder.decodeString.emap { className =>
27 Try(Class.forName(className)).toEither.left
28 .map {
29 case _: ClassNotFoundException =>
30 s"Simulation class '$className' not found on classpath. Check that the class exists and is compiled, or verify the fully qualified class name in your configuration."
31 case ex: Exception =>
32 s"Failed to load simulation class '$className': ${ex.getMessage}"
33 }
34 .flatMap { cls =>
35 Either.cond(
36 classOf[Simulation].isAssignableFrom(cls),
37 cls.asInstanceOf[Class[? <: Simulation]],
38 s"Class '$className' exists but does not extend io.gatling.core.scenario.Simulation. Ensure your simulation class extends Simulation."
39 )
40 }
41 }
42
43 given simulationConfigEncoder: Encoder[SimulationConfig] = new Encoder[SimulationConfig] {
44 def apply(config: SimulationConfig): Json = {
45 Json.obj(
46 "simulationClass" -> simulationClassEncoder(config.simulationClass),
47 "results" -> pathEncoder(config.results),
48 "sleepTime" -> Encoder.encodeString(config.sleepTime.toString),
49 "users" -> Encoder.encodeInt(config.users)
50 )
51 }
52 }
53 given simulationConfigDecoder: Decoder[SimulationConfig] = Decoder.instance { cursor =>
54 for {
55 simulationClass <- cursor
56 .get[Class[? <: Simulation]]("simulationClass")(using simulationClassDecoder)
57 results <- cursor.get[Path]("results")(using pathDecoder)
58 sleepTime <- cursor.get[FiniteDuration]("sleepTime")(using durationDecoder)
59 users <- cursor.get[Int]("users")
60 } yield SimulationConfig(simulationClass, results, sleepTime, users)
61 }
62}
1class ExampleTest extends Simulation {
2
3 private val config = ConfigManager.simulationConfig
4
5 scribe.info(s"Executing simulation: ${config.simulationClassName}")
6 scribe.info(s"Results will be saved to: ${config.results}")
7
8 private val httpProtocol = http
9 .baseUrl("https://api-ecomm.gatling.io")
10 .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
11 .doNotTrackHeader("1")
12 .acceptLanguageHeader("en-US,en;q=0.5")
13 .acceptEncodingHeader("gzip, deflate")
14 .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")
15
16 private val scn = scenario("BasicSimulation")
17 .exec(
18 http("request_1")
19 .get("/")
20 )
21 .pause(config.sleepTime)
22
23 setUp(
24 scn.inject(atOnceUsers(config.users))
25 ).protocols(httpProtocol)
26}
1//> using scala "3.7.2"
2//> using jvm "17"
3//> using dep "io.gatling:gatling-core:3.14.6"
4//> using dep "io.gatling:gatling-http:3.14.6"
5//> using dep "io.gatling:gatling-app:3.14.6"
6//> using dep "io.gatling.highcharts:gatling-charts-highcharts:3.14.6"
7//> using dep "com.lihaoyi::os-lib:0.11.5"
8//> using javaOpt "--add-opens=java.base/java.lang=ALL-UNNAMED"
9
10import io.gatling.core.Predef.*
11import io.gatling.http.Predef.*
12import scala.concurrent.duration.*
13import io.gatling.app.Gatling
14
15class BasicSimulation extends Simulation:
16
17 private val httpProtocol = http
18 .baseUrl("https://api-ecomm.gatling.io")
19 .acceptHeader(
20 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
21 )
22 .doNotTrackHeader("1")
23 .acceptLanguageHeader("en-US,en;q=0.5")
24 .acceptEncodingHeader("gzip, deflate")
25 .userAgentHeader(
26 "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0"
27 )
28
29 private val scn =
30 scenario("Scenario Name")
31 .exec(http("request_1").get("/"))
32
33 setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))
34
35@main
36def Runner(args: String*): Unit =
37
38 val results = os.RelPath("./.results/").resolveFrom(os.pwd)
39 val props: Array[String] =
40 Array(
41 "-rf", results.toString,
42 "-s", classOf[BasicSimulation].getName()
43 )
44
45 Gatling.main(props)
1scala-cli run example.scala
1scala-cli run https://gist.github.com/wiacekm/f050051f6270c67488c2343204fa9fb0
1mkdir $(pwd)/.results
2docker run \
3 -v $(pwd)/example.scala:/example.scala \
4 -v $(pwd)/.results:/.results \
5 -v $(pwd)/coursier-cache:/coursier-cache \
6 virtuslab/scala-cli \
7 --cache /coursier-cache \
8 /example.scala
1jobs:
2 build:
3 runs-on: ${{ matrix.OS }}
4 strategy:
5 matrix:
6 OS: ["ubuntu-latest", "macos-latest", "windows-latest"]
7 steps:
8 - uses: actions/checkout@v3
9 with:
10 fetch-depth: 0
11 - uses: coursier/cache-action@v6.4
12 - uses: VirtusLab/scala-cli-setup@v1.5
13 - run: scala-cli run example.scala
1image: virtuslab/scala-cli:latest
2# simple caching
3cache:
4 key: scala-cli-cache
5 paths:
6 - .scala-build/
7 - .bloop/
8 - .cache/
9 -
10run_perf_tests:
11 script:
12 - scala-cli run example.scala

Subscribe to our newsletter and never miss an article