Skip to content

Using Elementary

Matthias Ngeo edited this page Apr 2, 2021 · 12 revisions

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.

A Brief Tour of Elementary

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.

The JavacExtension

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 for ValidCase while process_int_field(...) will receive the results for both ValidCase and InvalidCase.

Supported Annotations

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 /.

The ToolsExtension

Supported Annotations

Clone this wiki locally