使用注解处理生成单元测试

5

我一直在寻找关于这个问题的信息,但我没有找到任何有用的资源。

我需要使用注解处理生成单元测试。我可以生成一个可以作为单元测试的类,但我不知道如何将这些生成的文件放置在正确的文件夹中。

默认情况下,输出将位于build/generated/source/apt/debug文件夹中,但我需要将这些文件放置在build/generated/source/apt/test中。我猜想。我的意思是以前我使用过注解处理,但我从未用它来生成单元测试,所以我不知道在哪里或如何定位它们的正确方法。

顺便说一句,我正在使用Android Studio 2.0。


1
我有一个解决方案。但是仅使用注释处理无法完成。不过,如果您的库用户,安装将会非常简单,甚至可能更简单。这对您可接受吗?或者您必须仅使用注释处理来完成它? - Xaver Kapeller
我刚刚发布了一个答案https://dev59.com/V5ffa4cB1Zd3GeqP_Kyf#38302923。但是这个解决方案对我来说并不理想,因为它仅限于在测试环境中定义的源中处理注释。所以,如果你的解决方案解决了这个限制,我将非常乐意接受你的答案;) - Víctor Albertos
XaverKapeller,你觉得什么时候可以发布答案? :D - Víctor Albertos
1
抱歉耽搁了,我现在正在工作中!一两个小时后回复你。 - Xaver Kapeller
我写了我的答案。结果比我想象的要长得多。希望这是你要找的! - Xaver Kapeller
3个回答

5
另一种选择是编写一个简单的Gradle插件,以配置项目以满足您的需求。通过编写自己的插件,您可以配置所需的所有内容,例如添加注释处理器的依赖项,然后修改javaCompile任务以将生成的依赖项移动到所需的文件夹中。
现在我意识到这可能看起来有些过度,但Gradle插件非常强大且相当易于制作。如果您能克服编写Groovy代码的初始学习曲线(我假设除了在build.gradle文件中外,您没有使用Groovy),那么它可以是实现您想要的功能的一种非常快速和简单的选项。
在我开始解释如何将Gradle插件与您的库结合使用之前,让我先解释一下我所做的事情:
我曾经编写过一个名为ProguardAnnotations的库,需要做的事情超出了注释处理器的范围。在我的情况下,我需要配置项目的proguard设置,以使用由我的注释处理器生成的proguard规则文件。实现插件并进行配置不费吹灰之力,除了配置proguard设置外,我还可以使用它来向项目添加注释处理器的依赖项。然后,我将该插件发布到Gradle插件存储库中,因此,要使用我的插件,只需在其build.gradle文件的顶部执行以下操作即可添加所有必需的依赖项并适当地配置项目:
plugins {
    id "com.github.wrdlbrnft.proguard-annotations" version "0.2.0.51"
}

你可以看到这样做可以使使用库变得非常简单。只需添加这个Gradle,它就会发挥其魔力,处理所有插件配置。


现在让我们来看一下插件本身。参考this link,可以查看我为我的库编写的Gradle插件。最终,您的插件应该看起来非常相似。
首先让我们看一下项目结构,为了简单起见,我将向您展示我为我的库编写的Gradle插件的屏幕截图。这应该是编写Gradle插件所需的最简单设置。

[Gradle Plugin Project Setup][1]

这里有三个重要的部分。Gradle选择使用Groovy作为脚本语言。所以你需要做的第一件事是在这里获取Groovy SDK:http://groovy-lang.org/download.html 我建议你使用IntelliJ来编写Gradle插件,但理论上Android Studio通过一些额外的配置也应该能够正常工作。
由于我们正在编写Groovy代码,您需要将代码放置在src/main/groovy文件夹中,而不是src/main/java。 您的源文件本身需要具有.groovy扩展名,而不是.java。 这里IntellIj相当棘手,即使您在src/main/groovy文件夹中工作,它仍然会首先提示您创建Java文件,只需注意文件名旁边图标的形状即可。 如果它是方形而不是圆形,则您正在处理Groovy文件。 除了编写Groovy代码之外,一切都很简单-每个有效的Java代码在Groovy中也是有效的-因此,您可以像在Java中一样开始编写代码,然后进行编译。 对于初学者,我不建议使用所有其他Groovy功能,因为可能会非常令人困惑。
另一个非常重要的部分是资源文件夹。在截图中,您可以看到文件夹src/main/resources/META-INF/gradle-plugins中的属性文件。这个属性文件确定了您的Gradle插件的id - 实际上是名称。它实际上非常简单:属性文件的名称就是您的Gradle插件的名称!截图中的属性文件名为com.github.wrdlbrnft.proguard-annotations.properties,因此我的Gradle插件名称为com.github.wrdlbrnft.proguard-annotations。如果您想在build.gradle文件中应用它,则可以在apply语句中使用该名称:apply project: 'com.github.wrdlbrnft.proguard-annotations'或如上面更进一步地在plugins部分中使用id
最后一部分是build.gradle本身。您需要配置它以能够编译groovy代码,并且需要所有Gradle插件所需的依赖项。幸运的是,您只需要五行代码:
apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

有了这个基本的build.gradle设置,也许需要稍微调整一下IDE设置,你就可以准备编写自己的Gradle插件了。


现在让我们创建插件类本身。选择一个像Java中的包名并创建一个适当的Groovy文件,例如YourLibraryPlugin.groovy。Gradle插件的基本样板看起来像这样:
package com.github.example.yourlibrary

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.ProjectConfigurationException

/**
 * Created by Xaver Kapeller on 11/06/16.
 */
class YourLibraryPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {

    }
}

现在,与Java相比,您的Groovy代码有两个不同之处:

  • 您不需要指定类的可见性。通常最好不要在代码中指定Java中的包本地可见性。但是,如果您希望,可以指定公共可见性,没有任何变化。
  • 如果您查看导入内容,可以看到每行末尾没有分号。在Groovy中,分号完全是可选的。您不需要它们。但是也可以使用它们,它们只是不必要的。

类本身是您的主要插件类。这是您的插件开始发挥作用的地方。一旦将插件应用于项目,就会调用apply(Project)方法。如果您曾经想详细了解build.gradle文件中的apply plugin: 'com.android.application'语句的作用-现在您已经有了答案。它们创建插件类的实例,并使用Gradle项目作为参数调用应用程序方法。

通常,在应用方法中,您想要做的第一件事是:

@Override
void apply(Project project) {
    project.afterEvaluate {

    }
}

现在,project.afterEvaluate 的意思是,在afterEvaluate后面的括号内的代码将在整个build.gradle被评估之后调用。这是一个好方法,因为你的插件可能依赖于应用于项目的其他插件,但开发人员可能已经将apply project: ...语句放在引用您的插件的语句apply project: ...之后。因此,通过调用afterEvaluate,您确保在执行任何操作之前至少进行了基本的项目配置,这可以避免错误并减少使用您的插件的开发人员的摩擦。但是不应该过度使用。您可以立即配置有关项目的所有内容。但是,在您的情况下,现在没有什么可做的,所以我们继续在afterEvaluate语句中。
现在你可以做的第一件事是为你的注释处理器添加依赖项。这意味着你的用户只需要应用插件,而不必担心添加任何依赖项。
@Override
void apply(Project project) {
    project.afterEvaluate {
        project.afterEvaluate {

            project.dependencies {
                compile 'com.github.wrdlbrnft:proguard-annotations-api:0.2.0.44'
                apt 'com.github.wrdlbrnft:proguard-annotations-processor:0.2.0.44'
            }
        }
    }
}

将依赖项添加到项目中的方法与build.gradle文件中的方法相同。您可以看到我在这里使用apt分类器来处理注释处理器。用户需要在项目中应用apt插件才能使其正常工作。然而,我留给您的练习是,您还可以检测apt插件是否已经应用于项目中,如果没有,则自动应用它!这是您的Gradle插件可以为用户处理的另一件事。

现在让我们来看看您希望Gradle插件实际执行的任务。最基本的要求是在注释处理器完成创建单元测试后做出响应。

因此,我们需要首先确定我们正在处理哪种类型的项目。它是一个Android库项目还是一个Android应用程序项目?这很重要,但由于原因比较复杂,我不会在本答案中进行解释,因为这会使这个已经很长的答案变得更加冗长。我只会展示代码并基本解释它的作用:

@Override
void apply(Project project) {
    project.afterEvaluate {
        project.afterEvaluate {

            project.dependencies {
                compile 'com.github.wrdlbrnft:proguard-annotations-api:0.2.0.44'
                apt 'com.github.wrdlbrnft:proguard-annotations-processor:0.2.0.44'
            }

            def variants = determineVariants(project)

            project.android[variants].all { variant ->
                configureVariant(project, variant)
            }
        }
    }
}

private static String determineVariants(Project project) {
    if (project.plugins.findPlugin('com.android.application')) {
        return 'applicationVariants';
    } else if (project.plugins.findPlugin('com.android.library')) {
        return 'libraryVariants';
    } else {
        throw new ProjectConfigurationException('The com.android.application or com.android.library plugin must be applied to the project', null)
    }
}

这段代码的作用是检查是否应用了com.android.library插件或者com.android.application插件,然后针对此情况迭代处理项目的所有变体。这意味着基本上你在build.gradle中指定的所有项目口味和构建类型都是独立配置的,因为它们实际上是不同的构建过程并需要各自的配置。 def与C#中的var关键字类似,可用于声明变量而无需显式指定类型。
project.android[variants].all { variant ->
    configureVariant(project, variant)
}

这部分是一个循环,它遍历所有不同的变体,然后调用configureVariant方法。在这个方法中,所有的魔法都发生了,这是你的项目实际重要的部分。让我们看一下基本实现:

private static void configureVariant(Project project, def variant) {
    def javaCompile = variant.hasProperty('javaCompiler') ? variant.javaCompiler : variant.javaCompile
    javaCompile.doLast {

    }
}

现在,该方法中的第一行是一个有用的代码片段,基本上只做了一件事:它返回Java编译任务。我们需要这个任务,因为注释处理是Java编译过程的一部分,一旦编译任务完成,您的注释处理器也完成了。javaCompile.doLast {} 部分类似于 afterEvaluate。它允许我们在任务结束时添加自己的代码!因此,在Java编译任务和注释处理完成后,就会执行 doLast 后面括号内的部分!在那里,您现在可以终于为您的项目做您需要做的事情。由于我不知道您需要做什么或如何做,我将给您一个示例:
private static void configureVariant(Project project, def variant) {
    def javaCompile = variant.hasProperty('javaCompiler') ? variant.javaCompiler : variant.javaCompile
    javaCompile.doLast {
        def generatedSourcesFolder = new File(project.buildDir, 'generated/apt')
        def targetDirectory = new File(project.buildDir, 'some/other/folder');
        if(generatedSourcesFolder.renameTo(targetDirectory)) {
            // Success!!1 Files moved.
        }
    }
}

这就是全部内容了!虽然这个回答很长,但只是浅尝辄止,如果我漏掉了什么重要的东西或者你有任何进一步的问题,请随时问我。
最后几件事情:
如果您需要将生成的文件移动到另一个文件夹中,您需要知道在apt文件夹中可能会有许多其他库生成的文件,通常将它们移开不是一个好主意。因此,您需要想出一个系统,以从文件夹中过滤出您的文件 - 例如一些常见的前缀或后缀。这应该不是问题。

还有一件事需要提醒:一旦你在configureVariants()方法中获取到了javaCompile任务,你实际上可以为你的注解处理器指定命令行参数,就像@emory所提到的那样。然而,这可能会相当棘手。事实上,这正是android-apt插件所做的。它通过在javaCompile任务上指定build/generated/apt文件夹作为所有注解处理器的输出文件夹来进行指定。再次强调,你不想搞砸它。我不知道有没有一种方法只为一个注解处理器 - 也就是你的注解处理器 - 指定输出文件夹,但可能有一种方法。如果你有时间,你可以查看android-apt的相关源代码here。处理器输出路径的指定发生在configureVariants方法下面。

在你的build.gradle中设置Gradle插件项目与任何其他Gradle项目非常相似,实际上非常容易。然而作为参考,这里是我编写Gradle插件所使用的完整build.gradle。如果你需要帮助来配置你的插件发布到jcenter或Gradle插件存储库,或者只是任何一般的配置,你可能会受益于查看:

buildscript {
    repositories {
        maven {
            url "https://plugins.gradle.org/m2/"
        }
        jcenter()
    }
    dependencies {
        classpath "com.gradle.publish:plugin-publish-plugin:0.9.4"
        classpath 'com.novoda:bintray-release:0.3.4'
    }
}

apply plugin: "com.gradle.plugin-publish"
apply plugin: 'com.jfrog.bintray'
apply plugin: 'maven-publish'
apply plugin: 'maven'
apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

final bintrayUser = hasProperty('bintray_user') ? property('bintray_user') : ''
final bintrayApiKey = hasProperty('bintray_api_key') ? property('bintray_api_key') : ''
final versionName = hasProperty('version_name') ? property('version_name') : ''

version = versionName

pluginBundle {
    vcsUrl = 'https://github.com/Wrdlbrnft/ProguardAnnotations'
    website = 'https://github.com/Wrdlbrnft/ProguardAnnotations'
    description = 'Makes dealing with Proguard simple and easy!'
    plugins {

        ProguardAnnotationsPlugin {
            id = 'com.github.wrdlbrnft.proguard-annotations'
            displayName = 'ProguardAnnotations'
            tags = ['android', 'proguard', 'plugin']
        }
    }
}

task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}

publishing {
    publications {
        Bintray(MavenPublication) {
            from components.java
            groupId 'com.github.wrdlbrnft'
            artifactId 'proguard-annotations'
            artifact sourcesJar
            version versionName
        }
    }
}

bintray {
    user = bintrayUser
    key = bintrayApiKey
    publications = ['Bintray']
    pkg {
        repo = 'maven'
        name = 'ProguardAnnotationsPlugin'
        userOrg = bintrayUser
        licenses = ['Apache-2.0']
        vcsUrl = 'https://github.com/Wrdlbrnft/ProguardAnnotations'
        publicDownloadNumbers = true
        version {
            name = versionName
            released = new Date()
        }
    }
}

如果您对build.gradle文件中未定义的所有三个或四个变量感到困惑-这些变量是在运行构建时由我的构建服务器注入的。在开发过程中,它们会自动回退到一些默认值。
我希望能帮助您使您的库更出色 :)

我以前从未得到过这样的答案 :) 谢谢,我非常非常感激你为帮助我的库所做的努力。当然,我将尝试这个解决方案。但是,我有一个问题,如何在不部署到Maven的情况下本地使用插件?我的意思是,在创建插件的过程中,如何使用它来检查它是否真正起作用,然后再发布它?谢谢! - Víctor Albertos
1
你可以像使用其他库一样将它部署到本地的maven仓库中。Groovy代码编译成普通的jar文件。你可以使用maven-publish插件的本地发布任务(我建议使用),或者如果你像我一样使用相同的设置,也可以使用bintray插件的本地发布任务,但除非你真的想要部署到jcenter,否则最好坚持使用通常的maven-publish插件。如果我记得正确,maven-publish插件的本地发布任务是publishToMavenLocal - Xaver Kapeller
1
我很高兴能够帮助你! - Xaver Kapeller
是的,你确实做到了 ;) 谢谢。 - Víctor Albertos

2

man页面上说:

-s dir 指定生成的源文件放置的目录。该目录必须已经存在;javac不会创建它。如果一个类是一个包的一部分,编译器将把源文件放在反映包名称的子目录中,并根据需要创建目录。例如,如果您指定-s /home/mysrc并且类被称为com.mypackage.MyClass,则源文件将被放置在/home/mysrc/com/mypackage/MyClass.java。

我想这就是你要找的。

但是,如果您的某些注释正在生成应放在一个目录中的单元测试,而您的某些注释正在生成应放在另一个目录中的生产代码,则我认为此解决方案将无法正常工作。


不,我的注释只是用来生成单元测试的。你回答的问题在于似乎我的库的用户需要配置Android Studio来将此参数传递给编译器。我考虑的是一种更内部的解决方案,不需要任何配置 :) - Víctor Albertos

0

使用 Android apt-plugin 的解决方案是使用 testApt 而不是 apt,正如在此 issue 中建议的那样。

然而,这会限制要处理的类的范围仅限于当前测试环境,这并不是我所需要的,但对大多数用户来说可能已经足够了。


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