Google compile testing tutorial
Introduction⌗
- Today’s topic is going to be in the form of a little tutorial on how to use the Google compile testing library, it can be used to compile Java source files and make assertions about compilation results (warnings, errors, generated files …), and it integrates very well with annotation processors.
Adding compile testing library to your project⌗
You can add the library to your test dependency list in
pom.xml
if you are using maven orbuild.gradle
if you are using gradle.<dependency> <groupId>com.google.testing.compile</groupId> <artifactId>compile-testing</artifactId> <version>0.17</version> <scope>test</scope> </dependency>
API description⌗
The library comes with a small and rich API:
com.google.testing.compile.Compiler
: which provides an API for compiling Java source files, with providing the ability to customize compilation options.com.google.testing.compile.Compilation
: which represents an Immutable object that is the result of a compilation process, and it gives you access to the List ofjavax.tools.Diagnostics
Which mainly represents a problem at some position in a given file, could be a warning or an errorcom.google.testing.compile.CompilationSubject
,com.google.testing.compile.JavaFileObjectSubject
: These are Google Truth subjects for fluent and easier assertions over both theCompilation
andJavaFileObject
objects.
Under the hood⌗
- Essentially the library will spare you from writing all the boilerplate
associated with compiling Java files and dealing with classpath setting,
resource allocation and freeing. The java files provided are compiled using an
instance of
javax.tools.JavaCompiler
. - Compiling a bunch of
JavaSourceFile
instances is done in the following steps:Creates an
com.google.testing.compile.InMemoryJavaFileManager
which is an implementation of thejavax.tools.StandardJavaFileManager
required by the JavaCompiler tool.Constructs the classpath from a list of given files (if any are specified).
Creates an instance of a
javax.tools.JavaCompiler.CompilationTask
which describes a future Compilation task that can be further configured before it gets started with thejava.util.Locale
that is going to be used when formatting diagnostic messages, and the list of annotation processorsjavax.annotation.processor.Processor
to be applied as part of this compilation operation.Creates a new Compilation object from the results of running the
CompilationTask
and returning it so that it can be used for tests.
Usage example⌗
- Let’s start with some simple examples to familiarize ourselfs with the API described above.
- Now imagine we want to test if some Java code generator is actually generating compilable Java code:
public interface CodeGenerator {
String generate(OperationContext context);
}
And we want to test weather the code compiles without any errors, this can be done as follows:
@Test
void testSuccessfullCodeGenerator() {
Compilation compilation = Compilation.javac()
.compile(JavaFileObjects.forSourceString(targetClassName, generator.generate(mockContext));
CompilationSubject.assertThat(compilation).succeeded();
}
Now suppose we want to add some annotation processors that will process some java test files that we have as part of our test resources, and make assertions on the generated files:
@Test
void testSuccessfullCodeGenerator() {
SomeAnnotationProcessor pr = new SomeAnnotationProcessor();
Compilation compilation = Compilation.javac()
.withProcessors(pr)
.compile(JavaFileObjects.forResource("TestFile.java"))
CompilationSubject.assertThat(compilation).succeeded();
CompilationSubject.assertThat(compilation)
.generatedSourceFile("TestFileGen.java")
.hasSourceEquivalentTo("TestFileGenExpected.java");
}
The forResource
method is pretty handy, it will do a classpath lookup on
a file named TestFile.java
and it will use it as the source for compilation.
In general you have multiple types of resources that you can use to create a compilation object:
- Java source files via
JavaFileObjects.forResource("SomeJavaFile.java")
. - Java jar files via
JavaFileObject.forResource("some-jar.jar")
. - Java Html docs via
JavaFileObject.forResource("some-docs.html")
. - Java source files in the form of simple Java strings via
JavaFileObjects.forString(...)
orJavaFileObjects.forSourceLines(...)
.
You can also have finer grain assertions over the Compilations with an
annotation processors via the utilities provided by CompilationSubject
class.
@Test
void testFinerGrainAnnotationProcessorCompilationSubject() {
SomeAnnotationProcessor pr = new SomeAnnotationProcessor();
JavaFileObject targetFile = JavaFileObjects.forResource("TestFile.java");
Compilation compilation = Compilation.javac()
.withProcessors(pr)
.compile(targetFile);
CompilationSubject.assertThat(compilation).failed();
// Asserting the number of errors
CompilationSubject.assertThat(compilation)
.hadErrorCount(2);
// You can fine grain error messages much as you want
CompilationSubject.assertThat(compilation)
.hadErrorContainingMatch("Some Error regex[0-9]")
.inFile(targetFile)
.onLine(10);
}
The library also offers a more thorough way to test the generated files:
@Test
void testFinerGrainAnnotationProcessorJavaSourceObjectSubject() {
SomeAnnotationProcessor pr = new SomeAnnotationProcessor();
JavaFileObject targetFile = JavaFileObjects.forResource("TestFile.java");
Compilation compilation = Compilation.javac()
.withProcessors(pr)
.compile(targetFile);
CompilationSubject.assertThat(compilation).succeeded();
ImmutableList<JavaFileObject> generatedFiles = compilation.generatedFiles();
// mix and match with junit core assertions
assertEquals(1, generatedFiles.size());
JavaFileObject generatedFile = generatedFiles.get(0);
// This is not a line by line checking, but an AST comparison between the two
// source files, pretty cool 😇
JavaFileObjectSubject.assertThat(generatedFile)
.hasSourceEquivalentTo(someFileObject);
}
Conclusion⌗
- Using the Compile testing library is a good idea to cleanup tests that require Java source file compilation, The library provides all the basic blocks for doing so, and you can extend it for your use case by writing your own Junit extensions and rules.