Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate unit test using annotation processing

I’ve been seeking information about this matter but I couldn’t find any useful resources.

I need to generate unit tests using annotation processing. I have no problem generating a class which can be a unit test. The thing I do not know how to do is placing these generated files in the right folder.

By default, the outputs will be located in the build/generated/source/apt/debug folder, but I need that these files will be placed on build/generated/source/apt/test. I guess. I mean I used before annotation processing, but I never used to generate unit tests, so I don’t know what is the right way to proceed about where or how to located them.

By the way, I'm using Android Studio 2.0.

like image 473
Víctor Albertos Avatar asked Jun 02 '16 22:06

Víctor Albertos


1 Answers

Another option you have is to write a simple Gradle plugin which configures the project to your needs. By writing your own plugin you can configure everything you need, like adding the dependencies for your annotation processor and then for example modify the javaCompile task to move your generated dependencies to the folder you want.

Now I realise that this might seem excessive, however Gradle plugins are very powerful and quite easy to make. If you can get over the initial learning curve of writing Groovy code (I assume you are not using Groovy besides in your build.gradle files) than it can be a very quick and easy option to do what you want to do


Before I start explaining how you can use a Gradle plugin in combination with your library let me explain what I was doing:

I once wrote a library called ProguardAnnotations which needed to do more than was possible with an annotation processor alone. In my case I needed to configure the proguard settings of the project to use a proguard rules file which is generated by my annotation processor. It was not much work to implement the plugin and beside configuring the proguard settings I could also use it to add the dependencies for my annotation processor to the project. I then published the plugin to the Gradle Plugin Repository so now to use my plugin which would add all required dependencies and configure the project appropriately all the users had to do was this to the top of their build.gradle file:

plugins {
    id "com.github.wrdlbrnft.proguard-annotations" version "0.2.0.51"
}

So you can see how this can make it very simple to use your library. Just by adding this Gradle would work its magic and take care of all of the plugin configuration.


Now lets take a look at the plugin itself. For reference this link will take you to the Gradle plugin I wrote for my library. Your plugin should look quite similar in the end.

Lets first look at the project structure, for simplicities sake I will show you a screenshot of the Gradle plugin I wrote for my library. This should be this simplest setup required for a Gradle plugin:

[Gradle Plugin Project Setup][1]

There are three important parts here. Gradle uses Groovy as its scripting language of choice. So the first thing you need to do is get the Groovy SDK here: http://groovy-lang.org/download.html

I recommend you use IntelliJ for writing Gradle plugins, but in theory Android Studio should work just as well with some addition configuration.

Since we are writing groovy code you need to place your code in the src/main/groovy folder instead of src/main/java. Your source files itself need to have a .groovy extension instead of .java. IntellIj is quite tricky here, since even if you are working in the src/main/groovy folder it will still always primarily prompt you to create java files, just look out for the shape of the icon next to the file name. If it is a square shape instead of a round shape then you are dealing with a groovy file. Aside from the writing Groovy code is pretty straight forward - every valid Java code is also valid in Groovy - so you can just start writing code like you are used to in Java and it will compile. For starters I don't recommend using all the additional Groovy features since it can be quite confusing.

The other very important part is the resources folder. In the screenshot you can see the a properties file in the folder src/main/resources/META-INF/gradle-plugins. This properties file is what determines the id - essentially the name - of your Gradle plugin. It is essentially very simple: The name of the properties file is the name of your Gradle plugin! The properties file in the screenshot is called com.github.wrdlbrnft.proguard-annotations.properties so the name of my Gradle plugin is com.github.wrdlbrnft.proguard-annotations. If you wanted to apply it in your build.gradle file you would use that name in your apply statements: apply project: 'com.github.wrdlbrnft.proguard-annotations' or as seen further above as id in the plugins section above!

The final part is the build.gradle itself. You need to configure it to be able to compile groovy code and you need all the dependencies required for Gradle plugins. Fortunately all you need is just five lines of code:

apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

With this basic setup in your build.gradle and maybe some slight fiddling with your IDE settings you should be ready to write own Gradle plugin.


Now lets create the plugin class itself. Choose a package name like in Java and create an appropriate groovy file, like for example YourLibraryPlugin.groovy. The basic boilerplate for a Gradle plugin looks like this:

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) {

    }
}

Now two things are different here in your Groovy code compared to Java:

  • You don't need to specify the visibility of your classes. Not specifying anything which would be package local visibility in Java code is usually the best choice. However you can specify public visibility if you want, nothing changes.
  • If you look at the imports you can see that there are no semicolons at the end of each line. In Groovy semicolons are purely optional. You don't need them anywhere. But having is ok too. They are just not required.

The class itself is your main Plugin class. Its where your plugin starts to do its magic. The apply(Project) method is called as soon as your plugin is applied to the project. If ever wanted to know in detail what the apply plugin: 'com.android.application' statements in your build.gradle file do - now you have your answer. They create an instance of the plugin class and call the apply method with the Gradle project as parameter.

Usually the first thing you want to do in your apply method is this:

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

    }
}

Now project.afterEvaluate means that the code inside the brackets following the afterEvaluate is called after the whole build.gradle has been evaluated. This is a good thing to do since your plugin might depend on other plugins being applied to the project, but the developer might have put the apply project: ... statements after the statement apply project: ... which is referencing your plugin. So in other ways by calling afterEvaluate you are making sure that at least the basic project configuration has happend before you do anything and this avoids erros and reduces friction for developers using your plugin. But you shouldn't overdo it. Everything you can configure about the project immediately should be immediately. In your case however there is nothing to do now, so we continue in the afterEvaluate statement.

The first thing you can do right now is for example add the dependencies for your annotation processor. So that means that your users just need to apply the plugin and don't have to worry about adding any dependencies themselves.

@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'
            }
        }
    }
}

Adding the dependencies to the project works just like in your build.gradle file. You can see that I use the apt classifier here for the annotation processor. Your users need to have the apt plugin also applied to the project for this to work. However what I leave as a exercise for you is that you can also detect if the apt plugin has already been applied to the project and if it hasn't automatically apply it! Another thing your Gradle plugin can take care of for your users.

Now lets get to the actual thing you want your Gradle plugin to do. On the most basic level you need to do something in response to your annotation processor having finished creating your unit tests.

So the first thing we need to do is figure out what kind of project we are working with. Is it an android library project or an android application project? This is important for a kind of complicated reason which I am not going to explain in this answer since it would make this already long answer much much longer. I am just going to show you the code and explain basically what it does:

@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)
    }
}

What this does is that it checks if either the com.android.library plugin or the com.android.application plugin has been applied and then iterates through all the variants of the project for this case. This means that basically all project flavours and buildTypes you specified in your build.gradle are configured independently - since they are also essentially different build processes and need their own configuration. The def is similar to the var keyword in C# and can be used to declare variables without specifying the type explicitly.

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

This part is a loop which iterates over all the different variants and then calls a configureVariant method. In this method all of the magic happens which is the actual important part for your project. Let's take a look at the basic implementation:

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

    }
}

Now the first line in the method is a useful snippet which essentially does one thing: It returns the java compile task. We need this since annotation processing is part of the java compilation process and once the compile task has finished then your annotation processor has also finished. The javaCompile.doLast {} part is similar to afterEvaluate. It allows us to tack on our own code at the end of the task! So right after the java compile task and therefore the annotation processing has finished the part inside the brackets after doLast is executed! In there you can now finally do what you need to do for your project. Since I don't exactly know what you need to do or how you need to do it I am just going to give you an example:

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.
        }
    }
}

And that is it! While this is quite a long answer it only touches the surface of this whole topic so if I forgot something important or you have any further questions feel free to ask.

A few final thing however:

If you need to move your generated files to a different folder you need to be aware that in the apt folder might be many other generated files from other libraries and usually it wouldn't be a good thing to move them away. So you need to figure out a system to filter just your files from the folder - for example some common prefix or postfix. This shouldn't be a problem.

Another thing I need to mention: Once you have gotten a hold of the javaCompile task inside the configureVariants() method you can actually specify command line parameters for your annotation processor like @emory mentioned. However that can be quite tricky. In fact that is exactly what the android-apt plugin does. It specifies the build/generated/apt folder as output folder for all annotation processor by specifying it on the javaCompile task. Again you don't want to mess with that. I don't know of a way to specify the output folder for just one annotation processor - namely yours - but there might be a way. If you have time you might want to look into it. You can look at the relevant source code of the android-apt here. The specifying of the processor output path happens down below in the configureVariants method.

Setting up a Gradle plugin project in your build.gradle very similar to any other Gradle project and actually quite easy. However as a reference, here is the complete build.gradle I use for the Gradle plugin I wrote. If you need help figuring out how to setup the publishing of your plugin to jcenter or the Gradle Plugin Pepository or just any general configuration you might benefit from taking a look:

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()
        }
    }
}

If you are confused about all the three or four variables in their which are defined nowhere in the build.gradle file - those are injected by my build server when I run a build. They automatically fallback to some default values while developing.

I hope I could help you make your library amazing :)

like image 194
Xaver Kapeller Avatar answered Sep 24 '22 15:09

Xaver Kapeller