Gatling Maven and Gradle Integration: Build Tool Setup Guide
The Gatling bundle is fine for exploration, but production load testing belongs in your build system. Maven and Gradle integration gives you dependency management, versioned simulations alongside application code, and clean CI/CD pipeline integration. This guide covers both build tools with complete, working configurations.
Why Build Tool Integration Matters
Running Gatling from the bundle directory has several problems:
- Gatling version is pinned to the downloaded bundle, not tracked in version control
- Simulations live outside the project, making code review harder
- CI/CD pipelines need the bundle pre-installed or scripted download logic
- No dependency management if simulations need third-party libraries
Build tool integration solves all of these. Your simulations are Scala source files in src/test/scala, managed like any other code, with Gatling declared as a dependency in pom.xml or build.gradle.
Maven Integration
Project Structure
load-tests/
├── pom.xml
└── src/
└── test/
└── scala/
└── simulations/
├── ApiLoadTest.scala
└── UserJourneyTest.scala
└── resources/
├── gatling.conf
└── data/
└── users.csvpom.xml Configuration
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>load-tests</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<gatling.version>3.11.5</gatling.version>
<gatling-maven-plugin.version>4.9.0</gatling-maven-plugin.version>
<scala-maven-plugin.version>4.9.1</scala-maven-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>io.gatling.highcharts</groupId>
<artifactId>gatling-charts-highcharts</artifactId>
<version>${gatling.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<!-- Scala compiler -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>${scala-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<arg>-deprecation</arg>
<arg>-feature</arg>
<arg>-unchecked</arg>
</args>
<jvmArgs>
<jvmArg>-Xss100M</jvmArg>
</jvmArgs>
</configuration>
</plugin>
<!-- Gatling plugin -->
<plugin>
<groupId>io.gatling</groupId>
<artifactId>gatling-maven-plugin</artifactId>
<version>${gatling-maven-plugin.version}</version>
<configuration>
<!-- Run specific simulation; omit to choose interactively -->
<simulationClass>simulations.ApiLoadTest</simulationClass>
<!-- Optional: override results directory -->
<resultsFolder>${project.build.directory}/gatling</resultsFolder>
<!-- Fail the build on assertion failures -->
<failFast>true</failFast>
<!-- JVM arguments for the Gatling process -->
<jvmArgs>
<jvmArg>-Xms512m</jvmArg>
<jvmArg>-Xmx2g</jvmArg>
<jvmArg>-XX:+UseG1GC</jvmArg>
</jvmArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>Running with Maven
# Run the configured simulation
mvn gatling:test
# Run a specific simulation (overrides pom.xml)
mvn gatling:test -Dgatling.simulationClass=simulations.UserJourneyTest
# Pass system properties to simulations
mvn gatling:test \
-Dtarget.url=https://api.staging.example.com \
-Dusers=200 \
-Dduration=300
# Skip Gatling in normal test runs
mvn test -Dgatling.skip=true
# Run only Gatling (skip unit tests)
mvn gatling:test -DskipTests=trueMaven Profiles for Different Environments
Use Maven profiles to manage environment-specific configurations:
<profiles>
<profile>
<id>staging</id>
<properties>
<target.url>https://api.staging.example.com</target.url>
<users>100</users>
<duration>300</duration>
</properties>
</profile>
<profile>
<id>production-smoke</id>
<properties>
<target.url>https://api.example.com</target.url>
<users>10</users>
<duration>60</duration>
</properties>
</profile>
<profile>
<id>load</id>
<properties>
<target.url>https://api.staging.example.com</target.url>
<users>1000</users>
<duration>1800</duration>
</properties>
</profile>
</profiles>Run with: mvn gatling:test -Pstaging
Gradle Integration
Project Structure
load-tests/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
└── gatling/
└── scala/
└── simulations/
└── ApiLoadTest.scala
└── resources/
└── data/
└── users.csvsettings.gradle.kts
rootProject.name = "load-tests"
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}build.gradle.kts
plugins {
scala
id("io.gatling.gradle") version "3.11.5.2"
}
repositories {
mavenCentral()
}
gatling {
// JVM arguments for the Gatling process
jvmArgs = listOf("-Xms512m", "-Xmx2g", "-XX:+UseG1GC", "-Xss8m")
// System properties available in simulations
systemProperties = mapOf(
"target.url" to (System.getProperty("target.url") ?: "https://api.staging.example.com"),
"users" to (System.getProperty("users") ?: "100"),
"duration" to (System.getProperty("duration") ?: "300")
)
}
dependencies {
// Add additional dependencies if simulations need them
gatling("com.github.javafaker:javafaker:1.0.2")
gatling("org.apache.commons:commons-lang3:3.14.0")
}Running with Gradle
# Run all simulations in src/gatling/scala
./gradlew gatlingRun
# Run a specific simulation
./gradlew gatlingRun-simulations.ApiLoadTest
# With system properties
./gradlew gatlingRun -Dtarget.url=https://api.example.com -Dusers=500
# List available simulation tasks
./gradlew tasks --group=gatlingGradle Build Variants
For different load profiles, use Gradle task configuration:
// build.gradle.kts
tasks.register("smokeTest") {
group = "gatling"
description = "Smoke test: low users, short duration"
doFirst {
System.setProperty("users", "5")
System.setProperty("duration", "60")
}
finalizedBy("gatlingRun-simulations.ApiLoadTest")
}
tasks.register("loadTest") {
group = "gatling"
description = "Full load test"
doFirst {
System.setProperty("users", "500")
System.setProperty("duration", "1800")
}
finalizedBy("gatlingRun-simulations.ApiLoadTest")
}Run with: ./gradlew smokeTest
Simulation Code for Build Tool Integration
With Maven or Gradle, simulations can read system properties cleanly:
// src/test/scala/simulations/ApiLoadTest.scala (Maven)
// src/gatling/scala/simulations/ApiLoadTest.scala (Gradle)
package simulations
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ApiLoadTest extends Simulation {
// Read from system properties with defaults
private val targetUrl = System.getProperty("target.url", "https://api.example.com")
private val users = System.getProperty("users", "50").toInt
private val durationSecs = System.getProperty("duration", "300").toInt
private val p95Threshold = System.getProperty("p95.threshold", "500").toInt
val httpProtocol = http
.baseUrl(targetUrl)
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.header("X-Test-Run", System.getProperty("test.run.id", "local"))
val scn = scenario("API Load Test")
.exec(
http("Health Check")
.get("/health")
.check(status.is(200))
)
.pause(500.milliseconds)
.exec(
http("List Items")
.get("/api/items?page=1&size=20")
.check(status.is(200))
.check(jsonPath("$.items").exists)
)
.pause(1, 3)
.exec(
http("Get Item")
.get("/api/items/1")
.check(status.is(200))
)
setUp(
scn.inject(
rampUsers(users).during(durationSecs.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile(95).lt(p95Threshold),
global.successfulRequests.percent.gt(99.0)
)
}GitHub Actions Integration
Maven workflow
name: Load Tests
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options: [staging, production]
users:
description: 'Number of virtual users'
required: false
default: '100'
schedule:
- cron: '0 3 * * 1-5' # Weekdays at 3am UTC
jobs:
load-test:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Run load tests
run: |
mvn gatling:test \
-Dtarget.url=${{ secrets.STAGING_API_URL }} \
-Dusers=${{ github.event.inputs.users || '100' }} \
-Dduration=300 \
-Dp95.threshold=500
env:
API_KEY: ${{ secrets.LOAD_TEST_API_KEY }}
- name: Upload Gatling results
uses: actions/upload-artifact@v4
if: always() # Upload even on failure
with:
name: gatling-results-${{ github.run_number }}
path: target/gatling/
retention-days: 30
- name: Comment on PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const files = fs.readdirSync('target/gatling');
const runDir = files[0];
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Load Test Results\n\nGatling run complete. [Download report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`
});Gradle workflow
name: Load Tests (Gradle)
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'load-tests/**'
jobs:
load-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: load-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Run Gatling simulations
run: |
./gradlew gatlingRun \
-Dtarget.url=${{ secrets.STAGING_API_URL }} \
-Dusers=100 \
-Dduration=300
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: gatling-results
path: load-tests/build/reports/gatling/Multi-Module Maven Setup
For larger projects, load tests often live in a separate module:
my-app/
├── pom.xml # Parent POM
├── app/
│ ├── pom.xml
│ └── src/main/java/
├── load-tests/
│ ├── pom.xml # Gatling module
│ └── src/test/scala/Parent pom.xml:
<modules>
<module>app</module>
<module>load-tests</module>
</modules>
<properties>
<gatling.version>3.11.5</gatling.version>
</properties>Load tests pom.xml:
<parent>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>load-tests</artifactId>
<!-- Reference app classes if needed -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<!-- ... Gatling dependencies ... -->
</dependencies>This lets simulations use application model classes for type-safe request body construction.
Caching Dependencies
Large Gatling projects benefit from caching Scala compiler and dependencies:
Maven caching
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven' # Caches ~/.m2/repositoryGradle caching
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle' # Caches ~/.gradle/caches
# Additional Scala compiler cache
- uses: actions/cache@v4
with:
path: ~/.ivy2
key: ${{ runner.os }}-ivy2-${{ hashFiles('**/build.gradle.kts') }}Generating Reports in CI
Gatling generates reports automatically. To make them accessible:
GitHub Pages deployment:
- name: Deploy report to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/main'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: target/gatling/
destination_dir: load-test-reports/${{ github.run_number }}S3 upload:
- name: Upload report to S3
run: |
aws s3 sync target/gatling/ \
s3://my-reports-bucket/gatling/${{ github.run_number }}/ \
--acl private
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Troubleshooting Common Issues
Scala compilation failures
[ERROR] error: not found: value scenarioCheck that import io.gatling.core.Predef._ and import io.gatling.http.Predef._ are at the top of your simulation. Also verify the Gatling version in pom.xml/build.gradle matches the plugin version.
Out of memory during test
java.lang.OutOfMemoryError: Java heap spaceIncrease the JVM heap in plugin configuration:
<!-- Maven -->
<jvmArgs>
<jvmArg>-Xmx4g</jvmArg>
</jvmArgs>// Gradle
gatling {
jvmArgs = listOf("-Xmx4g")
}StackOverflow with complex simulations
java.lang.StackOverflowErrorIncrease the Scala compiler stack size:
<!-- Maven: scala-maven-plugin configuration -->
<jvmArgs>
<jvmArg>-Xss100M</jvmArg>
</jvmArgs>Resource files not found
Ensure CSV files and other resources are in src/test/resources/ (Maven) or src/gatling/resources/ (Gradle). The Gatling plugin configures the classpath correctly for these locations.
Choosing Between Maven and Gradle
Choose Maven when:
- Your existing Java/Scala project uses Maven
- You need detailed lifecycle control (run load tests only in specific Maven phases)
- Your team is more familiar with XML configuration
- You want the battle-tested, widely documented approach
Choose Gradle when:
- Your existing project uses Gradle
- You want more concise build configuration (Kotlin DSL vs XML)
- You need flexible task composition (custom
smokeTest,loadTesttasks) - You prefer the faster incremental build performance Gradle offers
Both integrations are well-maintained and supported. The choice usually comes down to what your project already uses.
Next Steps
With build tool integration in place, the logical next steps are:
- Distributed testing: A single machine has limits. When you need to generate more load than one machine can produce, Gatling supports distributed test execution—covered in a separate guide.
- Monitoring integration: Stream Gatling metrics to Grafana/InfluxDB for real-time visibility.
- Test data management: Use database-backed feeders or external data services for realistic, varied load test data.
Your simulations are now first-class code. They have version history, code review, and reproducible builds—the same rigor you apply to the application under test.