An annotation processor is a plug-in to the Java compiler itself. So in order to debug a processor it either needs to run inside the compiler itself or be provided with a mock Compiler API.

The Compiler API is rather complex and extensive to mocking it, even using mocking libraries such as Mockito, is not viable.

Fortunately running the Java compiler from inside an unit test is actually easier than you would think. The JDK includes the Java Tools API which allows you to start a Java compiler from inside any Java code. Providing, of course, the code is ran within an JDK rather than a JRE.

First step is to set up a unit test with a temporary directory for the compiler to use. JUnit 5 comes with this functionality out of the box.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class ProcessorTest {

    @TempDir
    Path tempDir;

    @Test
    public void test() {
    }
}

This creates a new temporary directory every time the test is run and cleans up afterwards.

Creating the compiler and a task to run is pretty straight forward.

var compiler = ToolProvider.getSystemJavaCompiler();

var task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);

The complexity comes in setting up all the different pieces the compiler task needs.

Start with the diagnostics collector, which as the name suggests, collects diagnostics such as errors.

var diagnostics = new DiagnosticCollector<JavaFileObject>();

Next the file manager, which needs the diagnostics collector and can be configured with file location. For a simple test just setting up class and source outputs should be enough.

var fileManager = compiler.getStandardFileManager(diagnostics, null, null);
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(tempDir.toFile()));
fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, Arrays.asList(tempDir.toFile()));

Next we need something to compiler, called the compilation unit. For this I am just using a simple test class and since I am using Maven the file is in predictable location.

var file = new File("src/test/java/TestBean.java");
var compilationUnits = fileManager.getJavaFileObjects(file);

Now we can set up an annotation processor skeleton.

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedAnnotationTypes("*")
public class Processor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

Don’t forget, for the processor to work outside the unit test you need to put a ServiceLoader configuration file in META-INF/services or use AutoService.

Finally register the processor.

task.setProcessors(Collections.singletonList(new Processor()));

And call the task.

boolean success = task.call();

Now use your favorite assertion library to assert the task was succesful and check the expected files have been created in the temporary directory.

You can also print the diagnostics info.

for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
    System.err.println(diagnostic);
}

That’s it. Remember that the temporary directory is cleaned as soon as the test completes. So if you want to inspect it manually set a breakpoint on one of the asserts and examine it at your leisure.

The full unit test:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;

import static org.assertj.core.api.Assertions.*;

public class ProcessorTest {

    @TempDir
    Path tempDir;

    @Test
    public void test() throws IOException {
        // Given
        var compiler = ToolProvider.getSystemJavaCompiler();

        var diagnostics = new DiagnosticCollector<JavaFileObject>();

        var fileManager = compiler.getStandardFileManager(diagnostics, null, null);
        fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(tempDir.toFile()));
        fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, Arrays.asList(tempDir.toFile()));

        var file = new File("src/test/java/TestBean.java");
        var compilationUnits = fileManager.getJavaFileObjects(file);

        var task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);
        task.setProcessors(Collections.singletonList(new Processor()));

        // When
        boolean success = task.call();

        // Then
        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
            System.err.println(diagnostic);
        }
        assertThat(success).isTrue();
    }
}