如何为Java注解处理器编写自动化单元测试?

33

我正在尝试使用Java注解处理器进行实验。我可以使用"JavaCompiler"(实际上我目前正在使用"hickory")编写集成测试。我可以运行编译过程并分析输出。问题在于:即使我的注解处理器中没有任何代码,单个测试也需要大约半秒钟的时间,这太长了,不能以TDD方式使用。

对我来说,模拟依赖关系似乎非常困难(我需要模拟整个"javax.lang.model.element"包)。有人成功为注解处理器(Java 6)编写单元测试吗?如果没有...你会采用什么方法?

7个回答

62

这是一个老问题,但似乎注解处理测试的状态并没有得到改善,因此我们今天发布了Compile Testing。最好的文档在package-info.java中,但总的想法是当使用注解处理器运行时,有一种流畅的API可以测试编译输出。例如:

ASSERT.about(javaSource())
    .that(JavaFileObjects.forResource("HelloWorld.java"))
    .processedWith(new MyAnnotationProcessor())
    .compilesWithoutError()
    .and().generatesSources(JavaFileObjects.forResource("GeneratedHelloWorld.java"));

测试处理器是否生成与类路径上的GeneratedHelloWorld.java(黄金文件)匹配的文件。您还可以测试处理器是否产生错误输出:


JavaFileObject fileObject = JavaFileObjects.forResource("HelloWorld.java");
ASSERT.about(javaSource())
    .that(fileObject)
    .processedWith(new NoHelloWorld())
    .failsToCompile()
    .withErrorContaining("No types named HelloWorld!").in(fileObject).onLine(23).atColumn(5);

这明显比模拟简单,而且与典型的集成测试不同,所有输出都存储在内存中。


哇,这是一个非常棒的小库!示例现在有点不同了,使用CompilationassertThat而不是上面的语法。我花了一段时间才弄清楚assertThat并不是指AssertJ的assertThat,而是来自库本身的静态导入。 - Nils Breunese

10

你说得对,使用模拟库(如easymock)来嘲笑注释处理API是很困难的。我尝试过这种方法,但很快就失败了。你需要设置太多的方法调用期望值。测试变得难以维护。

基于状态的测试方法 对我来说相当有效。我必须为我的测试实现所需的javax.lang.model.* API部分。(只有不到350行代码。)

这是测试启动javax.lang.model对象的部分。在设置之后,该模型应与Java编译器实现处于相同状态。

DeclaredType typeArgument = declaredType(classElement("returnTypeName"));
DeclaredType validReturnType = declaredType(interfaceElement(GENERATOR_TYPE_NAME), typeArgument);
TypeParameterElement typeParameter = typeParameterElement();
ExecutableElement methodExecutableElement = Model.methodExecutableElement(name, validReturnType, typeParameter);

静态工厂方法定义在实现javax.lang.model.*类的Model类中,例如declaredType。(所有不支持的操作都将抛出异常。)
public static DeclaredType declaredType(final Element element, final TypeMirror... argumentTypes) {
    return new DeclaredType(){
        @Override public Element asElement() {
            return element;
        }
        @Override public List<? extends TypeMirror> getTypeArguments() {
            return Arrays.asList(argumentTypes);
        }
        @Override public String toString() {
            return format("DeclareTypeModel[element=%s, argumentTypes=%s]",
                    element, Arrays.toString(argumentTypes));
        }
        @Override public <R, P> R accept(TypeVisitor<R, P> v, P p) {
            return v.visitDeclared(this, p);
        }
        @Override public boolean equals(Object obj) { throw new UnsupportedOperationException(); }
        @Override public int hashCode() { throw new UnsupportedOperationException(); }

        @Override public TypeKind getKind() { throw new UnsupportedOperationException(); }
        @Override public TypeMirror getEnclosingType() { throw new UnsupportedOperationException(); }
    };
}

测试的其余部分验证了被测试类的行为。
Method actual = new Method(environment(), methodExecutableElement);
Method expected = new Method(..);
assertEquals(expected, actual);

你可以查看Quickcheck @Samples和@Iterables源代码生成器测试的源代码。(代码尚不完美。Method类有太多参数,而Parameter类没有在自己的测试中进行测试,而是作为Method测试的一部分进行测试。尽管如此,它应该说明了这种方法。)
祝好运!

5

jOOR是一个小型Java反射库,还提供了简化的访问javax.tool.JavaCompiler内存中Java编译API的功能。我们为此添加了支持以对jOOQ的注释处理器进行单元测试。您可以轻松编写像这样的单元测试:

@Test
public void testCompileWithAnnotationProcessors() {
    AProcessor p = new AProcessor();

    try {
        Reflect.compile(
            "org.joor.test.FailAnnotationProcessing",
            "package org.joor.test; " +
            "@A " +
            "public class FailAnnotationProcessing { " +
            "}",
            new CompileOptions().processors(p)
        ).create().get();
        Assert.fail();
    }
    catch (ReflectException expected) {
        assertFalse(p.processed);
    }
}

以上示例取自此博客文章


1
我之前遇到了同样的问题,并找到了这个问题。虽然其他提供的答案还不错,但我觉得还有改进的空间。基于此问题的其他答案,我创建了Elementary,这是一个JUnit 5扩展套件,为单元测试提供了真正的注解处理环境。
大多数库通过运行它们来测试注解处理器。然而,大多数注解处理器都非常复杂,并被分成更细粒度的组件。通过运行注解处理器无法测试单个组件。相反,我们将注解处理环境提供给这些测试。
以下代码片段说明了如何测试Lint组件:
import com.karuslabs.elementary.junit.Cases;
import com.karuslabs.elementary.junit.Tools;
import com.karuslabs.elementary.junit.ToolsExtension;
import com.karuslabs.elementary.junit.annotations.Case;
import com.karuslabs.elementary.junit.annotations.Introspect;
import com.karuslabs.utilitary.type.TypeMirrors;

@ExtendWith(ToolsExtension.class)
@Introspect
class ToolsExtensionExampleTest {

    Lint lint = new Lint(Tools.typeMirrors());
    
    @Test
    void lint_string_variable(Cases cases) {
        var first = cases.one("first");
        assertTrue(lint.lint(first));
    }
    
    @Test
    void lint_method_that_returns_string(Cases cases) {
        var second = cases.get(1);
        assertFalse(lint.lint(second));
    }
    
    @Case("first") String first;
    @Case String second() { return "";}
    
}

class Lint {
    
    final TypeMirrors types;
    final TypeMirror expectedType;
    
    Lint(TypeMirrors types) {
        this.types = types;
        this.expectedType = types.type(String.class);
    }
    
    public boolean lint(Element element) {
        if (!(element instanceof VariableElement)) {
            return false;
        }
        
        var variable = (VariableElement) element;
        return types.isSameType(expectedType, variable.asType());
    }
    
}

通过在测试类上注释@Introspect,并在测试用例上注释@Case,我们可以在同一文件中声明测试用例。测试使用Cases可以检索测试用例的相应Element表示形式。
如果有人感兴趣,我写了一篇文章 关于注解处理器问题 ,详细说明了单元测试注解处理器的问题。

1
我曾经遇到过类似的情况,所以我创建了Avatar库。它可能无法提供纯单元测试的性能,但如果使用正确,你不应该看到太大的性能损失。
Avatar允许你编写源文件、注释它,并将其转换为单元测试中的元素。这使你能够对消耗Element对象的方法和类进行单元测试,而无需手动调用javac。

0

一种选择是将所有测试捆绑在一个类中。对于给定的一组测试,编译等需要半秒钟的时间是恒定的,我认为测试的实际运行时间可以忽略不计。


0

我使用了http://hg.netbeans.org/core-main/raw-file/default/openide.util.lookup/test/unit/src/org/openide/util/test/AnnotationProcessorTestUtils.java,尽管这是基于java.io.File的简单性,因此具有您抱怨的性能开销。

Thomas建议模拟整个JSR 269环境将导致纯单元测试。您可能希望编写更多的集成测试,以检查处理器在javac内部实际运行的方式,从而更有保证地确保其正确性,但仅想避免磁盘文件。这将需要您编写一个模拟的JavaFileManager,这并不像看起来那么容易,我没有现成的示例,但您不需要模拟其他东西,如Element接口。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接