Annotation processors can be useful as a hacky workaround to get some language feature into the Java language.
jOOQ also has an annotation processor that helps validate SQL syntax for:
- Plain SQL usage (SQL injection risk)
- SQL dialect support (prevent using an Oracle only feature on MySQL)
You can read about it more in detail here.
Unit testing annotation processors
Unit testing annotation processors is a bit more tricky than using them. Your processor hooks into the Java compiler and manipulates the compiled AST (or does other things). If you want to test your own processor, you need the test to run a Java compiler, but that is difficult to do in a normal project setup, especially if the expected behaviour for a given test is a compilation error.
Let’s assume we have the following two annotations:
@interface A {}
@interface B {}
And now, we would like to establish a rule that
@A
must always be accompanied by
@B
. For example:
// This must not compile
@A
class Bad {}
// This is fine
@A @B
class Good {}
We’ll enforce that with an annotation processor:
class AProcessor implements Processor {
boolean processed;
private ProcessingEnvironment processingEnv;
@Override
public Set<String> getSupportedOptions() {
return Collections.emptySet();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton("*");
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
@Override
public void init(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement e1 : annotations)
if (e1.getQualifiedName().contentEquals(A.class.getName()))
for (Element e2 : roundEnv.getElementsAnnotatedWith(e1))
if (e2.getAnnotation(B.class) == null)
processingEnv.getMessager().printMessage(ERROR, "Annotation A must be accompanied by annotation B");
this.processed = true;
return false;
}
@Override
public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
return Collections.emptyList();
}
}
Now, this works. We can easily verify that manually by adding the annotation processor to some Maven compiler configuration and by annotating a few classes with A and B. But then, someone changes the code and we don’t notice the regression. How can we unit test this, rather than doing things manually?
jOOR 0.9.10 support for annotation processors
jOOR is our little open source reflection library that we’re using internally in jOOQ
jOOR has a convenient API to invoke the
javax.tools.JavaCompiler
API through
Reflect.compile()
. The most recent release 0.9.10 now takes an optional
CompileOptions
argument where annotation processors can be registered.
This means, we can now write a very simple unit test as follows (and if you’re using Java 15+, you can profit from text blocks!
For a Java 11 compatible version without text blocks, see our unit tests on github):
@Test
public void testCompileWithAnnotationProcessors() {
AProcessor p = new AProcessor();
try {
Reflect.compile(
"org.joor.test.FailAnnotationProcessing",
"""
package org.joor.test;
@A
public class FailAnnotationProcessing {
}
""",
new CompileOptions().processors(p)
).create().get();
Assert.fail();
}
catch (ReflectException expected) {
assertTrue(p.processed);
}
Reflect.compile(
"org.joor.test.SucceedAnnotationProcessing",
"""
package org.joor.test;
@A @B
public class SucceedAnnotationProcessing {
}
""",
new CompileOptions().processors(p)
).create().get();
assertTrue(p.processed);
}
So easy! Never have regressions in your annotation processors again!
Like this:
Like Loading...
Published by lukaseder
I made jOOQ
View all posts by lukaseder
Or you could use google/compile-testing (has been around for quite a while):
https://github.com/google/compile-testing
Yes, but NIH
Hi, nice post
i would like to add 2 comments here:
1- Do you really think that Lombok is a good annotation processing example, since they are not really using APT specifications.
2- In the processor example above you are throwing a runtime exception when not both annotation are present on the class, but the recommended way of highlighting errors with annotation processors is to use the messager obtained from the ProcessingEnvironment and pass the element causing the error in the message log, which helps IDEs to mark the element.
Thanks
Thanks for your comment, Ahmad.
You’re right about 1 and 2. I’ve deleted that sentence and added a remark.
It’s all great and awesome, and I really like the idea. But how could I assert on generated sources contents, provided that my annotation processor does generate some?
You could call the logic
I have the same question as skapral here. When i used compile() and my processor generated some source files how can i instantiate instances of the generated classes to test them? Your answer does not really help me here. Can you give an example?
I don’t know what to tell you folks here. I don’t have the answer, I haven’t really thought about the use-case of generating new source code from annotation processors yet… Look at the implementation of org.joor.Compile. It’s only 280 lines of code.
It’s a very simple wrapper around an existing set of JDK classes. The wrapper simplifies 80% of all use cases, while certainly not being good enough for the 20%, if it’s not working for you, why not just fall back to calling the JavaCompiler directly?
I hope this helps
I think you could create a utility method like this to compile the code, using text blocks and lombok:
and use it like this:
I’m not sure if this is really an answer to the previous discussion or to this blog post? It’s not related to annotation processing… And you don’t need any new methods, just use Reflect.create(), which calls the constructor.
Unfortunately it doesn’t func. I try so hard, but don’t can. Do I must to put this code in the same project of annotation process? Because it fail in the first validation. Thanks!
Thanks for your message. Hard to say why it fails from your description.
The examples assume the processor is in src/main/java, and the tests are in src/test/java. You can find some examples here, where both the test and the processor are in src/test/java: https://github.com/jOOQ/jOOR/blob/main/jOOR/src/test/java/org/joor/test/CompileOptionsTest.java
Maybe, try ask a specific question with some details on how to reproduce your problem on https://stackoverflow.com ?
Good luck!