Gatling Maven and Gradle Integration: Build Tool Setup Guide

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.csv

pom.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=true

Maven 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.csv

settings.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=gatling

Gradle 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/repository

Gradle 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 scenario

Check 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 space

Increase the JVM heap in plugin configuration:

<!-- Maven -->
<jvmArgs>
  <jvmArg>-Xmx4g</jvmArg>
</jvmArgs>
// Gradle
gatling {
  jvmArgs = listOf("-Xmx4g")
}

StackOverflow with complex simulations

java.lang.StackOverflowError

Increase 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, loadTest tasks)
  • 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:

  1. 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.
  2. Monitoring integration: Stream Gatling metrics to Grafana/InfluxDB for real-time visibility.
  3. 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.

Read more

Start now free