public class BehaviorTester extends Object
Annotation processors that generate complex APIs from small template classes are difficult to write good tests for. For an example, take a processor that generates a builder for a class. Comparing generated source with a golden output, whether via exact line-by-line comparison, AST comparison or bytecode comparison, leads to fragile tests. Adding a new method—say, buildPartial()—or changing the way an unset field is represented internally—say, from a null value to an explicit boolean field—will break every test, even though the user-visible behavior is unaltered.
Additionally, as test code will not compile without the processor, compilation becomes part of the test. Moving part of the test out of JUnit loses convenient integration with a number of tools and ADEs.
Behavioral testing verifies the generated code by compiling and running a small Java test against it. Well-written behavioral tests will check specific contracts the code generator must honor—e.g. "the data object returned will contain the values set on the builder", or "an exception will be thrown if a field has not been set"—contracts that will continue to hold even as internal representations change, and new features are added.
BehaviorTester takes a set of annotation processors and Java classes, and runs the JVM's own compiler against them. It also takes test code, wraps it in a static method, compiles and invokes it. As we assume the classes and test code will not compile without the annotation processors, we take them in as strings, which may be hard-coded in the test or read in from a resource file. Here is an example for a hypothetical Builder generator:
newBehaviorTester().with(builderGeneratingProcessor).with(newSourceBuilder() .addLine("package com.example;") .addLine("@%s public TestClass { ", MakeMeABuilder.class) .addLine(" private final %s<%s> strings;", List.class, String.class) .addLine(" TestClass(TestClassBuilder builder) {") .addLine(" strings = builder.getStrings();") .addLine(" }") .addLine(" public %s<%s> getStrings() {", List.class, String.class) .addLine(" return strings;") .addLine(" }") .addLine("}") .build()).with(newTestBuilder() .addLine("com.example.TestClass instance = new com.example.TestClassBuilder()") .addLine(" .addString(\"Foo\")") .addLine(" .addString(\"Bar\")") .addLine(" .build();") .addLine("assertEquals(\"Foo\", instance.getStrings().get(0));") .addLine("assertEquals(\"Bar\", instance.getStrings().get(1));") .build())runTest();
| Constructor and Description |
|---|
BehaviorTester() |
| Modifier and Type | Method and Description |
|---|---|
void |
runTest()
Compiles, loads and tests everything given to
with(javax.annotation.processing.Processor). |
BehaviorTester |
with(JavaFileObject compilationUnit)
Adds a
JavaFileObject to pass to the compiler when runTest() is invoked. |
BehaviorTester |
with(Processor processor)
|
BehaviorTester |
withContextClassLoader()
Ensures
Thread.getContextClassLoader() will return a class loader containing the
compiled sources. |
public BehaviorTester with(Processor processor)
public BehaviorTester with(JavaFileObject compilationUnit)
JavaFileObject to pass to the compiler when runTest() is invoked.SourceBuilder,
TestBuilderpublic BehaviorTester withContextClassLoader()
Thread.getContextClassLoader() will return a class loader containing the
compiled sources. This is needed by some frameworks, e.g. GWT, but requires us to run tests
on a separate thread, which complicates exceptions and stack traces.public void runTest()
with(javax.annotation.processing.Processor).
Runs the compiler with the provided sources and processors. Loads the generated code into a
classloader. Finds all @Test-annotated methods (e.g. those built by TestBuilder) and invokes them. Aggregates all exceptions, and propagates them to the caller.
Copyright © 2015 inferred.org. All rights reserved.