Supercharge Your Java App: Performance Testing with Gatling

Sean CoughlinSean Coughlin
10 min read

Introduction

In modern software development, performance is as crucial as functionality. It’s no longer enough for an application to simply "work"; it needs to work well under pressure. Imagine launching a new app that starts slowing down or crashing when too many users log in at once. That’s a fast-track way to lose users and damage a brand.

This is where performance testing comes in. It’s a method to ensure your application can handle the real-world demands of its users. Performance testing helps you uncover weak points before your users do, giving you the insights you need to make sure your app is fast, stable, and scalable. In this post, we’ll dive into how you can use Gatling to test the performance of your Java applications, from getting started to writing effective tests.

But first, let’s talk about what performance testing actually is and why it matters.

Understanding Performance Testing

What is Performance Testing?

Performance testing is a way to evaluate how an application behaves when it's under different kinds of load. The goal isn't just to see if the application works, but to see how well it works under various conditions. Does it slow down when a certain number of users hit it at once? Does the response time stay within acceptable limits? Does it crash if pushed too hard? These are all questions performance testing aims to answer.

Different types of performance testing focus on different aspects of this behavior:

  • Load Testing: This simulates a typical number of users accessing the application at the same time. The goal here is to see if the system can handle its expected everyday usage without slowing down or failing.

  • Stress Testing: Stress testing takes things a step further by pushing the application beyond its normal capacity. It’s all about finding the breaking point—how many users or requests it takes before things start falling apart. The purpose here isn’t to simulate normal use but to understand how the application behaves under extreme conditions, like a spike in traffic.

Both of these are crucial for understanding the limitations and resilience of your system.

Why Performance Testing Matters

The reason performance testing is so important is simple: user experience. No matter how good your app’s features are, if it’s slow or unreliable, people won’t stick around. Performance problems often don’t show up until users actually start using the system, which can make them tricky to catch if you don’t test properly.

By running performance tests early and often, you can:

  • Spot bottlenecks before they hit production: It’s much easier to fix performance issues during development than it is once they’re live.

  • Ensure your system scales: As your user base grows, you want to be sure your app grows with it, without losing speed or reliability.

  • Improve stability: By stress-testing your system, you’ll learn how it behaves under extreme load and be able to prepare for real-world scenarios, whether it's a product launch or a sudden influx of users.

Ultimately, performance testing helps ensure that when your users hit the system hard, it holds up—and that’s key to building reliable software.

Introducing Gatling for Java Applications

What is Gatling?

Gatling is an open-source tool specifically designed for load and performance testing. It's often used to test how applications respond under heavy traffic. What sets Gatling apart is its ability to simulate a large number of users with minimal resource usage. This makes it an excellent tool for testing web applications, especially those built in Java.

While Gatling was originally written in Scala, it now provides a Java DSL, which is what we’ll focus on in this guide. The Java DSL allows you to write Gatling simulations in a way that feels natural to Java developers. The idea is to write performance tests just like you’d write any other Java code, without needing to dive into a new language.

Why Use Gatling for Java?

There are several reasons why Gatling stands out for testing Java applications:

  • Java DSL: You can write Gatling tests in Java, meaning you don't have to learn Scala if you don’t want to. This reduces the learning curve and lets you get started quickly.

  • Efficient and Lightweight: Gatling uses an asynchronous, non-blocking model, which allows it to simulate thousands of users with low memory usage.

  • Modular and Reusable: Gatling scenarios are easy to write and can be reused and extended, making it suitable for long-term projects with evolving performance testing needs.

Now, let’s move on to how you can set up Gatling in a Java project.

Setting Up Gatling in a Java Project with Gradle

Since we’re focusing on Java, we’ll use Gradle to manage dependencies and run the tests. Gatling works well with Gradle, and integrating it into your Java project is simple.

Adding Gatling Dependencies

First, you need to add the necessary dependencies to your build.gradle file. Here’s how you do that:

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'io.gatling.highcharts:gatling-charts-highcharts:3.9.5'
    testImplementation 'io.gatling:gatling-app:3.9.5'
}

This pulls in Gatling along with the Highcharts plugin for better visual reporting (even though we won’t focus on HTML reports here). The gatling-app dependency allows us to run the simulations.

Project Structure

The structure of your project will be similar to any other Java application. Here's what a basic setup might look like:

src
└── test
    └── java
        └── simulations
            └── BasicSimulation.java

Place your Gatling test files inside the src/test/java/simulations directory. This ensures they are properly picked up when you run your tests using Gradle.

Writing Your First Gatling Test in Java

Now that Gatling is set up in your project, let’s write a simple test to see how it works.

Creating a Basic Simulation

Here's a basic Gatling test written in Java that simulates a user making a GET request to a web page:

package simulations;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class BasicSimulation extends Simulation {
    HttpProtocolBuilder httpProtocol = http
        .baseUrl("http://example.com")  // Set the base URL
        .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        .acceptLanguageHeader("en-US,en;q=0.5")
        .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0");

    ScenarioBuilder scn = scenario("BasicSimulation")  // Define the scenario
        .exec(http("request_1")  // Name the request
        .get("/"))  // Send a GET request to the root of the URL
        .pause(1);  // Simulate a 1-second pause (user "think" time)

    {
        setUp(scn.injectOpen(atOnceUsers(10)))  // Simulate 10 users accessing the site at once
            .protocols(httpProtocol);  // Apply the HTTP protocol settings to this scenario
    }
}
  • HttpProtocolBuilder: This is where you configure your HTTP protocol settings. In this example, we’re setting the base URL to "http://example.com" and defining headers that are sent with every request.

  • ScenarioBuilder: The scenario is the "user journey." In this case, the user sends a simple GET request to the root ("/") of the website. After that, there’s a 1-second pause to simulate real user behavior.

  • setUp: This sets up the scenario and specifies how many users should be simulated. In this example, we're simulating 10 users hitting the website simultaneously using the atOnceUsers(10) injection profile.

Running the Test

To run the Gatling test using Gradle, use the following command:

./gradlew gatlingRun -Dgatling.simulationClass=simulations.BasicSimulation

This tells Gradle to execute the BasicSimulation class. If everything is set up correctly, the test will run, and you’ll see the results printed in the terminal.

Enhancing Your Gatling Tests

Once you’ve successfully run a basic simulation, you’ll want to start adding complexity to your tests. Real-world user behavior is rarely as simple as sending a single request, so let’s look at how you can simulate more realistic scenarios and add performance validation checks.

Simulating Realistic User Behavior

In this section, we’ll expand on the simple test by chaining together multiple requests to simulate a more realistic user journey.

Here’s an example of a scenario where a user visits a homepage, searches for a product, and then views a product detail page:

ScenarioBuilder scn = scenario("UserJourney")
    .exec(http("Home Page")
        .get("/"))  // Visit the home page
    .pause(2)  // Pause for 2 seconds
    .exec(http("Search Page")
        .get("/search?q=gatling"))  // Perform a search
    .pause(1)  // Pause for 1 second
    .exec(http("Product Page")
        .get("/products/1"));  // View a product detail page
  • We simulate a user visiting three different pages of the site, with pauses in between to mimic the natural "think time" of a real user.

  • Each HTTP request has a name (Home Page, Search Page, Product Page), making it easy to identify in the logs or results.

You can also add more complexity with POST requests, form submissions, and cookies to mimic user sessions. The idea here is to mirror actual user behavior as closely as possible.

Adding Assertions

Assertions are a powerful feature that allows you to define the performance criteria that your application should meet. For example, you can assert that the maximum response time should be under 2 seconds or that at least 99% of requests should succeed.

Here’s how you can add assertions to the simulation:

{
    setUp(scn.injectOpen(atOnceUsers(10)).protocols(httpProtocol))
        .assertions(
            global().responseTime().max().lt(2000),  // Assert that the max response time is less than 2 seconds
            global().successfulRequests().percent().gt(99.0)  // Assert that at least 99% of requests succeed
        );
}
  • global().responseTime().max().lt(2000): This ensures that no request takes longer than 2 seconds.

  • global().successfulRequests().percent().gt(99.0): This checks that 99% or more of the requests are successful.

By including assertions, you can automatically validate that your system is meeting its performance goals.

Best Practices for Performance Testing with Gatling

Now that you’re familiar with writing Gatling tests, let’s talk about best practices to ensure your performance tests provide the most value.

Clear Objectives

Before writing any tests, it’s important to know what you’re testing and why. Define clear objectives for each test:

  • Are you checking for acceptable response times?

  • Do you want to know the maximum load your system can handle?

  • Are you simulating real-world usage patterns?

By setting specific goals, you can create focused tests that provide actionable insights.

Simulating Realistic Scenarios

It’s essential that the scenarios you test resemble actual user behavior. Here are a few tips:

  • Use real-world data: If possible, base your test data on actual usage patterns or production logs. This ensures your tests reflect how users interact with your app.

  • Think time: Adding realistic pauses between requests (like in the pause() method) helps simulate the delay users naturally take between actions.

Monitor System Resources

While running performance tests, it’s not just the response times that matter. You should also keep an eye on system resources like CPU, memory, and network usage to see how your infrastructure handles the load.

Here’s where external monitoring tools (like Grafana, Prometheus, or JVisualVM) can come in handy. Monitoring helps ensure that performance issues are caught at the system level, not just from the perspective of response times.

Automating Performance Tests in CI/CD Pipelines

Performance testing shouldn’t be a one-off event. By integrating your Gatling tests into a CI/CD pipeline, you can run them automatically with every release. This way, performance regressions are caught early, before they hit production.

While we won’t go into detail here, many CI/CD tools (like Jenkins or GitLab CI) support running Gatling tests as part of your deployment process, ensuring that your app’s performance remains consistent over time.

Conclusion

We’ve covered the essentials of performance testing with Gatling for Java applications—from setting up the environment to writing and enhancing tests. Performance testing is a crucial step in delivering reliable, scalable applications that provide a smooth user experience under real-world conditions. Gatling’s Java DSL makes it easy for developers to integrate performance testing into their workflow without having to switch languages.

FAQs

Can Gatling test non-HTTP protocols?

Yes, while Gatling is primarily used for HTTP, it also supports protocols like WebSockets, JMS, and even custom protocols with some extra configuration.

How do I handle authentication in Gatling tests?

Gatling can handle various types of authentication, including basic authentication, OAuth, and even cookies or tokens. For basic authentication, you can add credentials like this:

httpProtocol.auth().basic("username", "password");

For token-based authentication, you can include the token in the headers of your request.

What’s the difference between load and stress testing?

  • Load Testing simulates the normal or expected number of users to see how the system performs under typical conditions.

  • Stress Testing pushes the system beyond its limits to find its breaking point. The goal is to see how the application behaves under extreme load and when it fails.

Resources

For further reading and to deepen your understanding of performance testing with Gatling, check out the following resources:

0
Subscribe to my newsletter

Read articles from Sean Coughlin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sean Coughlin
Sean Coughlin

Software Engineer