Annotations Processing, Pt 3: Testing
So you’ve got your processor up and running in front of you as described in my prior posts? Great! That makes the next part simple.
Now, normally you’d want to use tests to drive the development of your library, and writing an annotation processor is no different. You’ll want to have a set of test scenarios that must be met by your software, and you’ll in most cases you’ll want to gradually iterate towards fulfilling each scenario one after another until all of them are met.
In our case, we didn’t take that particular approach because:
- this is a simple learning exercise;
- the code itself is not very complicated (which is a terrible excuse, but the goal here is to take baby steps);
But since you’ve got your feet really quite wet, you’re probably wondering how you can demonstrate to both yourself and any subsequent developers that your processor meets the requirements. The easiest way to do this is by using a library called compile-testing. This library provides us with a fluent API for testing our processor and it totally beats the old-school way of testing your processor, which was utterly joyless task and bordered on Sisyphean magnitudes of difficulty. So open that up, grab yourself the bits for your build.gradle and resync.
After that, the first thing you need to do is either grab a class for your desired inputs and outputs, or if you’re like me you’ll just make some String arrays. You’ll want them to reflect any input classes you’re gonna plug in and the corresponding output code you want your annotation processor to spit out, and maybe a couple of other classes that reflect cases where your processor will error out. Below you’ll see I’ve made three different arrays:
private String[] srcStrings = {
"package test;",
"",
"import au.com.jtribe.comparator.lib.ComparableField;",
"",
"public class Test {",
" @ComparableField",
" String compared;",
"}"
};private String[] failureStrings = {
"package test;",
"",
"import au.com.jtribe.comparator.lib.ComparableField;",
"",
"public class Failure {",
" @ComparableField",
" String compared;",
"",
" @ComparableField",
" String compared2;",
"}"
};private String[] outputStrings = {
"package test;",
"",
"import java.lang.Override;",
"import java.util.Comparator;",
"",
"public class TestComparator implements Comparator<Test> {",
" @Override",
" public int compare(Test lhs, Test rhs) {",
" if (lhs.compared.compareTo(rhs.compared) > 0) {",
" return 1;",
" }",
" if (lhs.compared.compareTo(rhs.compared) < 0) {",
" return -1;",
" }",
" return 0;",
" }",
"}",
""
};
So now that we’ve got our code, we need to turn them Strings into something we can work on. compile-testing gives us a nifty utility class called JavaFileObjects that we can use to manufacture instances of a thing called JavaFileObject:
private JavaFileObject src = JavaFileObjects.forSourceLines("test.Test", srcStrings);private JavaFileObject output = JavaFileObjects.forSourceLines("test/TestComparator", outputStrings);private JavaFileObject failure = JavaFileObjects.forSourceLines("test.Failure", failureStrings);
You’ll notice that we address inputs using a proper package.Class scheme, whilst outputs are referred to using a directory scheme that corresponds to the package name and the class name contained in the source String. It’s a bit of a gotcha.
So what do we do with these mysterious JavaFileObjects, strange things they are? Looking at the documentation we can see that they’re basically just an abstraction of a Java class so with the right tools we should be able to come up with some tests that say “given this class as input, running this annotation processor, we should get this class as output”. Right?
If you said yes, then you’re absolutely correct — and we can use a library called Truth to do so. This is an assertion/proposition framework written by Google and it’s exactly what we need. It contains a particular method called assertAbout() that you can use in conjunction with compile-testing’s javaSource() method to chain together a number of conditions and assertions about the input and output of the processor, like the following:
assertAbout(javaSource())
.that(src)
.processedWith(new AnnotationsProcessor())
.compilesWithoutError()
.and()
.generatesSources(output);
There’s that fluent API I was talking about.
What we’re doing here is using an instance of our annotation processor to process the input and then compare the output with the expected output. Really simple stuff. And that’s literally a whole test! It’s also not particularly magical even though the underlying code might be a little dense.
Of course, we need to test for failure too. An implementation of such a test might be as follows:
@Test
public void testProcessorFailure() {
assertAbout(javaSource())
.that(failure)
.processedWith(new AnnotationsProcessor())
.failsToCompile()
.withErrorContaining("You can only have one ComparableField per class");
}
Next time we’ll experiment with AutoValue extensions and converting our processor into one.