eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Overview

Testing is an essential part of the application build process. Of course, it takes time, so we should look for ways to speed it up. The obvious way is to leverage multi-core processors and run tests in parallel.

In this tutorial, we’ll learn how to perform parallel testing with Gradle.

2. The Gradle Setup

Throughout this article, we’ll use Gradle version 9.

First, let’s set it up for parallel testing. We’ll start with the gradle.build file:

plugins {
    id 'java-library'
}

test {
    maxParallelForks = (int) (Runtime.runtime.availableProcessors() / 2 + 1)
    useJUnitPlatform {
        includeTags testForGradleTag
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

Note the maxParallelForks property set to the number of threads that will execute the tests in parallel. We use the Java Runtime availableProcessors() function to assign half and one available cores for tests.

Next, with the useJUnitPlatform property, we invoke and configure the JUnit 5 test environment. We use includeTags to allow filtering tests by the @Tag annotation. To complete the configuration, we need to set the Gradle testForGradleTag property to its default value. Let’s do that in the gradle.properties file:

testForGradleTag=serial

With this default setting, we’ll run only tests marked with the @Tag(“serial”) annotation. It’s an optional setting designed to make the testing more straightforward for us. It has nothing to do with Gradle test forking.

3. How It Works

To understand how parallel testing works, let’s create a test class called UnitTestClass1.

First, let’s prepare the functions to measure the execution time of a particular test and the whole suite:

@Tag("parallel")
@Tag("UnitTest")
public class UnitTestClass1 {

    private long start;
    private static long startAll;

    @BeforeAll
    static void beforeAll() {
        startAll = Instant.now().toEpochMilli();
    }

    @AfterAll
    static void afterAll() {
        long endAll = Instant.now().toEpochMilli();
        System.out.println("Total time: " + (endAll - startAll) + " ms");
    }

    @BeforeEach
    void setUp() {
        start = Instant.now().toEpochMilli();
    }

    private LocalTime localTimeFromMilli(long time) {
        return Instant.ofEpochMilli(time)
          .atZone(ZoneId.systemDefault())
          .toLocalTime();
    }
}

Next, let’s add four test methods, annotated with @Test. We name them whenAny_thenCorrect1() through whenAny_thenCorrect4(). Here is the first one:

@Test
public void whenAny_thenCorrect1() throws InterruptedException {
    Thread.sleep(1000L);
    assertTrue(true);
}

At this moment, they’re identical and do nothing but sleep for one second.

Finally, the tearDown() method provides test statistics:

@AfterEach
void tearDown(TestInfo testInfo) {
    long end = Instant.now().toEpochMilli();

    String name = testInfo.getDisplayName();
    System.out.println("Test " + name + " from class " + getClass().getSimpleName() +
      " started at " + localTimeFromMilli(start) + " ended at " + localTimeFromMilli(end) + 
      ": (" + (end - start) + " ms)");
}

We report both the test display name and the test class name to track the execution order.

3.1. One Test Class

To test only one class, let’s change the annotation of UnitTestClass1 to serial:

@Tag("serial")
@Tag("UnitTest")
public class UnitTestClass1

Then, we can start this class test with Gradle:

$ ./gradlew -i cleanTest test -PtestForGradleTag=serial | grep "Test whenAny"

We start ./gradlew with the -i option for the info debug level. Then we filter the output of the tearDown() method with grep. Let’s study this output:

Serial testing with Gradle-1

We see that individual test methods are run in serial, one after another. It’s clearly visible when we compare the start times of subsequent test methods, in the yellow frame.

3.2. Many Test Classes

To benefit from the parallel testing, we need many test classes, not methods. So, let’s just copy our UnitTestClass1 three times. We number new classes from 2 to 4 and ensure they’re annotated with @Tag(“parallel”). Then, we start the test again:

$ ./gradlew -i cleanTest test -PtestForGradleTag=parallel -PtestForGradleTag=UnitTest | grep "Test whenAny"
Parallel testing with Gradle-1

Now we have 16 test runs. When we go through the logs in chronological order, we see that test methods of different classes started almost simultaneously. So, Gradle runs three threads in parallel. On the other hand, if we sort out the output of one class, we find out that its methods are called in serial, as in the example with one class.

4. Working With Resources

When the test calls for a resource, we should guarantee a clear distinction between independent test runs. Let’s take a look at a simple class designed to create a folder:

public class FolderCreator {

    Boolean createFolder(Path path, String name) throws IOException {
        String newFolder = path.toAbsolutePath() + name;
        File f = new File(newFolder);
        return f.mkdir();
    }
}

The mkdir() function returns false if folder creation failed. It may happen if the folder already exists.

4.1. The Integration Test

Let’s test this method with the TestFolderCreator1 test class. As we interact with the operating system, we tagged this test as integration:

@Tag("parallel")
@Tag("integration")
public class TestFolderCreator1 {

    //time reporting helper functions as in UnitTestClass1

    private Path baseFolder = Paths.get(
      getClass()
      .getResource("/")
      .getPath());

    private Integer workerID = Integer.valueOf(System.getProperty("org.gradle.test.worker", "1"));
    private String testFolderName = "/" + "Test_" + workerID;

    @BeforeEach
    void setUp() {
        start = Instant.now().toEpochMilli();

        //preemptive clean up with helper function
        removeTestFolder();
    }

    private void removeTestFolder() {
        File folder = new File(
          baseFolder.toFile()
          .getAbsolutePath() + testFolderName);

        folder.delete();
    }
}

The structure of this class is similar to that of UnitTestClass1. However, in the setUp() method, we ensure that the test folder doesn’t exist, preemptively removing it.

Now let’s look at the test function whenCreated_ThenCorrect():

@Test
void whenCreated_ThenCorrect() throws IOException, InterruptedException {
    FolderCreator folderCreator = new FolderCreator();
    assertTrue(folderCreator.createFolder(baseFolder, testFolderName));
    Thread.sleep(1000L);
}

It asserts that the folder creation is successful. Unlike in the previous example, we have only one test function now.

Finally comes the tearDown() function:

@AfterEach
void tearDown(TestInfo testInfo) {
    long end = Instant.now().toEpochMilli();

    System.out.println(
        "Class " + getClass().getSimpleName() + " checks folder " + testFolderName +
          " started at " + localTimeFromMilli(start) + " ended at " + localTimeFromMilli(end) +
          ": (" + (end - start) + " ms)");

    //clean up with helper function
    removeTestFolder();
}

Note that this function removes the test folder after each test run. This is enough housekeeping in the serial testing scenarios.

4.2. Multiple Workers for Parallel Test

In a parallel case, we need to ensure that different threads don’t try to create the same folder. We accomplished that by reading the worker ID, set by Gradle in the org.gradle.test.worker system property:

System.getProperty("org.gradle.test.worker", "1")

With this ID, we could make up different folder names for each working thread.

As before, we create four copies of this class. Then, we run the test passing the additional integration tag to ./gradlew:

$ ./gradlew -i cleanTest test -PtestForGradleTag=parallel -PtestForGradleTag=integration | grep "Class TestFolder"

Let’s track the dependency between the test folder name and the worker thread in the output:

Folder creation test

We see that during one parallel run with three threads, each class has its own worker ID. However, after the worker pool is exhausted, ID equal to 373 is reused.

5. The Static Horror

Now, let’s examine an example of testing the function that relies on the application’s static state. We’ll use a simple Singleton pattern implementation:

public final class ClassSingleton {

    public String info = "Initial info class";
    private static ClassSingleton INSTANCE;

    private static int count = 0;

    private ClassSingleton() {
    }

    public static ClassSingleton getINSTANCE() {
        return INSTANCE;
    }

    public int getCount() {
        return count;
    }

    public static ClassSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ClassSingleton();
        }
        count++;
        return INSTANCE;
    }

    // more features below ...
}

We see the count field, which accumulates how many times the instance of ClassSingleton is requested.

Then, we’ll want to test if this counter is set correctly. So, let’s do that with:

@Test
public void whenOneRequest_thenSuccess() throws InterruptedException {
    ClassSingleton testSingleton = ClassSingleton.getInstance();
    assertEquals(1, testSingleton.getCount());
    Thread.sleep(1000L);
}

This test will fail. Or it will not fail. Or sometimes it fails, and sometimes it doesn’t. Playing with the number of test classes and threads, we can see that it’s simply unpredictable, therefore somewhat useless.

5.1. How to Fix It

This behaviour arises because static variables are defined within the Java Virtual Machine (JVM) scope. Such a virtual machine is started separately for each thread. However, test classes may retake the same thread. So, we can’t expect count to be equal to one if we run the testSingleton() function multiple times.

To make things even worse, we made count a private field and provided no setter for it. Therefore, we have no way to set the singleton state before the test (no matter how much sense it’d make).

Gradle comes to the rescue with the forkEvery property, which says how often the new thread should be started. If we set it to one, each test class will have its own thread and also its own JVM. To set this property, we need to edit the ‘test‘ section in the gradle.build file:

test {
    maxParallelForks = (int) (Runtime.runtime.availableProcessors() / 2 + 1)
    forkEvery = 1
    useJUnitPlatform {
        includeTags testForGradleTag
    }
}

Now our singleton test will always succeed. Moreover, this feature isn’t limited to parallel testing. If we touch on the static context in many test cases, we may also need it in the serial testing scenarios. We only need to locate tests in different classes.

Finally, we should be aware that starting a new JVM for each test class slows down the tests. We can disable this feature by setting forkEvery to 0, which is its default value.

6. Conclusion

In this article, we learned how to run parallel tests in Gradle. First, we configured Gradle for such a task. Then, we examined how the tests are performed. Based on this, we concluded that tests are parallel based on the test class rather than the method.

Next, we took a look at cases that are particularly troublesome in parallel testing. This included access to resources and handling the static state of the tested application. We saw how Gradle mitigated these problems.

Finally, we can regard parallel testing with Gradle as well-suited to speed up independent test runs.

As always, the code for the examples is available over on GitHub.

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)