Back

Debug Native Tests in Maven and Gradle Projects

This guide shows you how to set up and debug native tests in Java projects using the Maven or Gradle plugin for Native Image.

It focuses on debugging native test executables with GDB (best suited for Linux environments).

Using a small demo application and a JUnit test, the guide explains how to:

  • run tests as native executables
  • build native test executables with debug information
  • troubleshoot missing metadata
  • open a native test executable in GDB

Prepare a Demo Application

Prerequisites

Make sure you have installed a GraalVM JDK and that JAVA_HOME points to it. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.

The application and test sources below are the same for both the Maven and Gradle examples.

  1. Add the application class in src/main/java/org/graalvm/example/GreetingFormatter.java:
     package org.graalvm.example;
    
     public final class GreetingFormatter {
    
         private GreetingFormatter() {
         }
    
         public static String format(String name) {
             return "Hello, " + name.strip() + "!";
         }
     }
    
  2. Add the test class in the test source set, src/test/java/org/graalvm/example/GreetingFormatterTest.java:
     package org.graalvm.example;
    
     import org.junit.jupiter.api.Test;
    
     import static org.junit.jupiter.api.Assertions.assertEquals;
    
     class GreetingFormatterTest {
    
         @Test
         void formatsGreeting() {
             assertEquals("Hello, Native Image!", GreetingFormatter.format(" Native Image "));
         }
     }
    

    This test is intentionally simple so you can set breakpoints either in the test method or in GreetingFormatter.format().

Debug Maven Native Tests

The Maven plugin can build a native test executable and run JUnit Platform tests in that executable.

Configure Native Tests

Add the JUnit dependency, the Surefire plugin, and the Native Build Tools test goal to your pom.xml:

<properties>
    <native.maven.plugin.version>0.11.5</native.maven.plugin.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0</version>
        </plugin>
    </plugins>
</build>

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>${native.maven.plugin.version}</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>test-native</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                            <phase>test</phase>
                        </execution>
                    </executions>
                    <configuration>
                        <debug>true</debug>
                        <verbose>true</verbose>
                        <buildArgs>
                            <buildArg>-O0</buildArg>
                        </buildArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

The test goal builds a native test executable and runs the discovered JUnit Platform tests in that executable. Setting <debug>true</debug> enables debug information, and -O0 improves the stepping experience in GDB by disabling compiler optimizations.

Run the Native Tests

Run the test suite as a native executable:

mvn -Pnative test

The command builds the native test executable, runs the discovered tests, and stores the generated debug artifacts, gdb-debughelpers.py, next to that executable. Keep the build output so you can reopen the same binary in GDB after the test run completes.

Troubleshoot Missing Metadata

If the native test fails because of missing reflection, resource, serialization, or JNI metadata, add the following agent configuration to the Native Build Tools plugin <configuration> block:

<agent>
    <enabled>true</enabled>
    <metadataCopy>
        <outputDirectory>META-INF/native-image</outputDirectory>
        <merge>true</merge>
        <disabledStages>
            <stage>main</stage>
        </disabledStages>
    </metadataCopy>
</agent>

Then collect metadata with the tracing agent and rerun the native test:

mvn -Pnative -Dagent=true test
mvn -Pnative native:metadata-copy
mvn -Pnative test

The native:metadata-copy goal copies the collected test metadata into the location configured in <metadataCopy>. This is the recommended path when the JVM test passes but the native test fails during startup or when exercising dynamic features.

Open the Native Test Executable in GDB

Native Image debugging with GDB works best on Linux. On other platforms, especially macOS, it may require extra setup.

With debug info enabled, the build writes the native test executable to target/native-tests. Depending on the platform and debugger support, the build can also generate additional debug-related artifacts in the target/ directory.

Open it in GDB:

gdb target/native-tests

Set a breakpoint in the test or in the application code:

(gdb) break GreetingFormatterTest.java:10
(gdb) break GreetingFormatter.java:8
(gdb) run

Debug Gradle Native Tests

The Gradle plugin can build a native test executable and lets you configure that test binary separately from the main application binary.

Configure Native Tests

Add JUnit support and a graalvmNative configuration for the test binary in build.gradle:

plugins {
    id 'java'
    id 'org.graalvm.buildtools.native' version '0.11.5'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
}

test {
    useJUnitPlatform()
}

graalvmNative {
    binaries {
        test {
            imageName = 'greeting-tests'
            debug = true
            verbose = true
            buildArgs.add('-O0')
        }
    }
}

The nativeTest task builds and runs the native test binary. The debug = true setting enables debug information, and -O0 improves source-level stepping in GDB.

Run the Native Tests

Compile and run the test suite as a native executable:

./gradlew nativeTest

The native test executable is created in build/native/nativeTestCompile/greeting-tests.

If you want to build the binary first and inspect it manually, run:

./gradlew nativeTestCompile

Troubleshoot Missing Metadata

If the native test fails because of missing reflection, resource, serialization, or JNI metadata, enable the tracing agent for the JVM test run. You can do that on the command line with -Pagent=standard, or declare it in the graalvmNative block:

graalvmNative {
    agent {
        defaultMode = 'standard'
    }
}

Then run the JVM tests with the agent and copy the generated metadata into the test resources:

./gradlew -Pagent=standard test
./gradlew metadataCopy --task test --dir src/test/resources/META-INF/native-image
./gradlew nativeTest

For test-only metadata, copy the generated files into src/test/resources/META-INF/native-image/. If the metadata is needed by the main application instead, copy it into src/main/resources/META-INF/native-image/.

Open the Native Test Executable in GDB

Native Image debugging with GDB works best on Linux. On other platforms, especially macOS, it may require extra setup.

Once the test binary is compiled, open it in GDB:

gdb build/native/nativeTestCompile/greeting-tests

Then set breakpoints and start the test binary:

(gdb) break GreetingFormatterTest.java:10
(gdb) break GreetingFormatter.java:8
(gdb) run

If you use a custom test suite such as integrationTest, register a dedicated test binary for it and build it with the matching compile task, for example nativeIntegrationTestCompile. You can then open the generated binary in GDB and debug it the same way as the default test binary.

Summary

This guide showed how to configure native JUnit tests for Maven and Gradle projects, rebuild them with debug information, collect missing metadata with the tracing agent, and inspect the resulting test binaries in GDB.

Use GDB when you want to inspect a native executable at the symbol and source level, especially on Linux. Use JDWP when you want to attach a Java debugger to a native executable and work with familiar IDE debugging workflows.

For GDB-based workflows, see:

For JDWP-based workflows, see:

For plugin-specific testing options, see the Native Build Tools testing support for Maven and Gradle.

Connect with us