-
-
Notifications
You must be signed in to change notification settings - Fork 6
Using Elementary
This article describes how to use Elementary to unit test an annotation processor. We assume that the reader is acquainted with annotation processing and the javax.lang.model.*
packages.
Tests are required to be compiled against Java 11 although the classes under test can be compiled against earlier version of Java. In addition, the library requires a minimum JUnit version of 5.7.1
.
Do join Karus Labs' discord if you require any assistance.
At the heart of Elementary is the standalone compiler upon which everything else is built, including the JavacExtension
and ToolsExtension
JUnit extensions. Configuration of said extensions is done through annotations on the test classes and methods. In the interest of keeping things short and sweet, we shall skim over the standalone compiler since it is seldom used barring a few advanced cases. Most will find the higher-level JavacExtension
and ToolsExtension
more pleasant to use anyways.
For each test, JavacExtension
compiles a suite of test files with the given annotation processor(s). The results of the compilation are then funneled to the test method for subsequent assertions. All configuration is handled via annotations with no additional set-up or tear-down required.
We recommend using the JavacExtension
in the following scenerios:
- Black-box testing an annotation processor
- Testing the results of a compilation
- Testing an extremely simple annotation processor
A typical usage of the JavacExtension
will look similar to the following:
import com.karuslabs.elementary.Results;
import com.karuslabs.elementary.junit.JavacExtension;
import com.karuslabs.elementary.junit.annotations.Case;
import com.karuslabs.elementary.junit.annotations.Classpath;
import com.karuslabs.elementary.junit.annotations.Options;
import com.karuslabs.elementary.junit.annotations.Processors;
@ExtendWith(JavacExtension.class)
@Options("-Werror")
@Processors({ImaginaryProcessor.class})
@Classpath("my.package.ValidCase")
class ImaginaryTest {
@Test
void process_string_field(Results results) {
assertEquals(0, results.find().errors().count());
}
@Test
@Classpath("my.package.InvalidCase")
void process_int_field(Results results) {
assertEquals(1, results.find().errors().contains("Element is not a string").count());
}
}
@SupportedAnnotationTypes({"*"})
class ImaginaryProcessor extends AnnotationProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment round) {
var elements = round.getElementsAnnotatedWith(Case.class);
for (var element : elements) {
if (element instanceof VariableElement)) {
var variable = (VariableElement) element;
if (!types.isSameType(variable.asType(), types.type(String.class))) {
logger.error(element, "Element is not a string");
}
} else {
logger.error(element, "Element is not a variable");
}
}
return false;
}
}
Let’s break down the code snippet.
-
By annotating the test class with
@Options
, we can specify the compiler flags used when compiling the test cases. In this snippet,-Werror
indicates that all warnings will be treated as errors. -
To specify which annotation processor(s) is to be invoked with the compiler, we can annotate the test class with
@Processors
. -
Test cases can be included for compilation by annotating the test class with either
@Classpath
,@Resource
or@Inline
. Java source files on the classpath can be included using@Classpath
or@Resource
while strings inside@Inline
can be transformed into an inline source file for compilation. One difference between@Classpath
and@Resource
is how directories are separated.@Classpath
separates directories using.
while@Resource
uses/
. -
An annotation’s scope is tied to its target’s scope. If a test class is annotated, the annotation will be applied for all test methods in that class. On the same note, an annotation on a test method will only be applied on said method.
-
Results represent the results of a compilation. We can specify Results as a parameter of test methods to obtain the compilation results. In this snippet,
process_string_field(...)
will receive the results forValidCase
whileprocess_int_field(...)
will receive the results for bothValidCase
andInvalidCase
.
Annotation | Description |
---|---|
@Classpath |
Denotes a class on the current classpath to be included for compilation. Directories are separated by . . |
@Inline |
A string representation of a class to be included for compilation. |
@Options |
Represents the compiler flags to be used during compilation. |
@Processors |
The annotation processors to be applied during compilation. |
@Resource |
Denotes a file on the current classpath to be included for compilation. Directories are separated by / . |