【问题标题】:Generate unit test using annotation processing使用注释处理生成单元测试
【发布时间】:2016-06-02 22:42:54
【问题描述】:

我一直在寻找有关此问题的信息,但找不到任何有用的资源。

我需要使用注释处理生成单元测试。我生成一个可以作为单元测试的类没有问题。我不知道该怎么做是将这些生成的文件放在正确的文件夹中。

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

顺便说一句,我使用的是 Android Studio 2.0。

【问题讨论】:

  • 我有一个解决方案给你。然而,它不能单独使用注释处理来完成。但是,对于您的图书馆的用户来说,即使不是更简单,设置也同样简单。这对您来说是否可以接受,是否必须仅通过注释处理来完成?
  • 我刚刚发布了一个答案stackoverflow.com/a/38302923/1525990。但是这个解决方案对我来说并不理想,因为它将处理注释仅限于测试环境中定义的源。因此,如果您的解决方案解决了该限制,我将非常乐意接受您的回答;)
  • XaverKapeller 您认为何时可以发布答案? :D
  • 抱歉耽搁了,我现在正在上班!会在一两个小时内回复您。
  • 我写下了我的答案。结果比我想象的要长得多。我希望它是您正在寻找的!

标签: java android-studio junit annotation-processing


【解决方案1】:

您的另一个选择是编写一个简单的 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 使用 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 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 项目作为参数调用 apply 方法。

通常你想要在你的 apply 方法中做的第一件事是:

@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 中指定的所有项目风格和 buildTypes 都是独立配置的——因为它们也是本质上不同的构建过程并且需要它们自己的配置。 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 {

    }
}

现在方法中的第一行是一个有用的 sn-p,它本质上做了一件事:它返回 java compile 任务。我们需要这个,因为注解处理是 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-apthere的相关源码。处理器输出路径的指定发生在下面的 configureVariants 方法中。

在你的 build.gradle 中设置一个 Gradle 插件项目与任何其他 Gradle 项目非常相似,实际上非常简单。但是作为参考,这里是我用于我编写的 Gradle 插件的完整 build.gradle。如果您需要帮助确定如何将插件发布到 jcenter 或 Gradle Plugin Pepository 或任何常规配置,您可能会受益于查看:

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 的情况下在本地使用插件?我的意思是,在创建插件的过程中,我如何在发布之前使用它来检查它是否真的工作?谢谢!
  • 您可以像其他任何库一样将其部署到本地 maven 存储库。 Groovy 代码编译为普通的 jar 文件。您可以使用 maven-publish 插件的本地发布任务(我会推荐),或者如果您像我一样使用相同的设置,您也可以使用 bintray 插件的本地发布任务,但坚持使用通常的 maven-publish插件更好,除非你真的想部署到 jcenter。如果我没记错的话,maven-publish 插件的本地发布任务是publishToMavenLocal
  • 很高兴能为您提供帮助!
  • 是的。确实你做到了;)谢谢。
【解决方案2】:

man page

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

我想这就是你要找的。​​p>

但是,如果您的一些注释生成的单元测试应该放在一个目录中,而您的一些注释正在生成应该放在另一个目录中的生产代码,那么我认为这个解决方案将不起作用。

【讨论】:

  • 不,我的注释仅用于生成单元测试。您的答案的问题在于,我的库的用户似乎需要配置 Android Studio 才能将此参数传递给编译器。我正在考虑一个更内部的解决方案,它不需要任何配置:)
【解决方案3】:

使用 android apt-plugin 的解决方案是使用 testApt 而不是 apt,正如 issue 所建议的那样。

尽管如此,这限制了将要处理的类的范围限制在当前测试环境中,这不是我所需要的,但对于大多数用户来说可能就可以了。

【讨论】:

    猜你喜欢
    • 2017-05-22
    • 1970-01-01
    • 2017-03-04
    • 2013-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-23
    相关资源
    最近更新 更多