Java annotation processing tool – tips to writing and testing

Java annotation processing tool is a tool you can use to process annotations in the source code. All you need is to implement an annotation processor. However, these processors tend to be quite difficult both to understand and to test.

At first I tried to use Mockito to mock the parts of the API needed for my code to work. This ended up in a lot of setting up in the tests and it didn’t strike me as a lean solution. A change of requirements would lead to a lot of change in the test setup. After browsing the Internet I found an interesting post at stackoverflow that had several solutions to write better tests. One solution is to implement the parts of JSR 269 needed for your processors to execute. Since I’m quite lazy I kept searching for a simpler solution. In the end I found an informal post telling me about the tool Compile Testing. This tool utilizes the java compiler with the option to specify the annotation processors to be used when compiling. This is all done using a fluent API that is simple to use and easy to understand. I tried it out on a processor that prevents the possibility to put @Ignore on a unit test without stating the cause for ignoring it. I then wrote five simple examples showing what’s allowed, and what’s not allowed. These examples got one testcase each to verify that the processor behaves as expected.

When I had finished my tests I figured that they where quite easy to read while the annotation processor itself was quite difficult to understand. So I spent som time trying to express my code a little clearer, extracting the ugly bits into a supporting class and rephrasing some of the methods. The api itself can be quite difficult to understand, but I think most of it is now encapsulated in more expressive methods so that it’s quite clear what’s going on.

The examples below are excerptions from the original source code. The full source can be found in GitHub


In these test driven times I figure I should show you the tests first. This testcase is just a chained statement telling the assertion framework that when a given java source file is processed with the IgnoreAnnotationProcessor it should fail to compile. It also asserts that the compiler tells us which method or class that caused the processor to fail.

package com.visma.test.apt;

public class IgnoreAnnotationProcessorTest {

  @Test
  public void shouldNotCompileIgnoreAnnotatedMethodWithoutValue() {
    ASSERT.about(javaSource())
    .that(JavaFileObjects.forResource("com/visma/test/apt/unittest/IgnoreOnMethodWithoutReason.java"))
    .processedWith(new IgnoreAnnotationProcessor())
    .failsToCompile()
    .withErrorContaining("Cause missing for @Ignore [com.visma.test.apt.unittest.IgnoreOnMethodWithoutReason.someTestMethod()]");
  }
}

The source the above testcase compiles is just a simple empty test that has an ignored method.

package com.visma.test.apt.unittest;

import org.junit.Test;
import org.junit.Ignore;

public class IgnoreOnMethodWithoutReason {
  @Test
  @Ignore
  public void someTestMethod() {}
}

The processor itself is quite small. The hard bits of the code has been put into its own class. What I’m doing here is to loop all the classes and methods that is annotated with @Ignore. If the method or class doesn’t have a cause for being ignored it will print an error message to the provided processing environment.

package com.visma.test.apt;

@SupportedAnnotationTypes("org.junit.Ignore")
public class IgnoreAnnotationProcessor extends AbstractProcessor {

    private static final boolean WILL_NOT_CLAIM_ANNOTATIONS = false;

    @Override
    public boolean process(Set<? extends TypeElement> annotationElements, RoundEnvironment roundEnv) {
        for(IgnoreAnnotatedMethodOrClass ignoredMethodOrClass : fromRoundEnvironment(roundEnv)) {
            if(!ignoredMethodOrClass.hasCauseDescription()) {
                ignoredMethodOrClass.printErrorMessageTo(processingEnv);
            }
        }
        return WILL_NOT_CLAIM_ANNOTATIONS;
    }
}

Then it is the final piece of the puzzle. The IgnoreAnnotatedMethodOrClass has a static method to extract all the ignored elements from the round environment and it encapsulates the functionality to verify that the ignored element describes the cause for being ignored. It also has a method to print an error message to the given processing environment.

package com.visma.test.apt;

class IgnoreAnnotatedMethodOrClass {
  
  private static final String errorMessage = "Cause missing for @Ignore [%1$s]";
  private final Element methodOrClass;

  static Set fromRoundEnvironment(RoundEnvironment roundEnv) {
    Set ignoredElements = new HashSet();
    for (Element methodOrClass : roundEnv.getElementsAnnotatedWith(Ignore.class)) {
      ignoredElements.add(new IgnoreAnnotatedMethodOrClass(methodOrClass));
    }
    return ignoredElements;
  }

  boolean hasCauseDescription() {
    return getCauseForIgnore().isPresent();
  }

  void printErrorMessageTo(ProcessingEnvironment processingEnv) {
    String elementName = toString();
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, format(errorMessage, elementName));
  }
}

About

Tommy Bø er en erfaren seniorkonsulent i Visma Consulting og er fagmanager for faggruppen Software Craftsmanship. Tommy er generell ekspert på java og er opptatt av kodekvalitet, både at den skal være godt strukturert og lett å forstå og at den skal ha automatiserte tester som verifiserer forventet oppførsel og gjør koden lett å vedlikeholde. I tillegg jobber han mye med kontinuerlige leveranser og automatisering av prosesser rundt dette.
Connect with Tommy: