Custom Gradle Test Task For Android

一世执手 提交于 2019-12-11 04:07:57

问题


Problem

I want to create a custom gradle test task to only run JUNIT tests and omit Robolectric tests. I am attempting to achieve this task by creating a new test annotation and omitting any tests that include that new annotation.

Error

JUNIT packages are not included when I run the the gradle task.

error: package android.test.suitebuilder.annotation does not exist
import android.test.suitebuilder.annotation.SmallTest;

Details

New Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RobolectricTest {
}

Gradle File

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

repositories {
    mavenCentral()
    maven { url     'http://artifactory.ops.am1.qa.ext.bamgrid.com/artifactory/mobile-resources' }
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.google.gms:google-services:1.3.0-beta1'
    }
}

android {
    compileSdkVersion rootProject.ext.compileSDKVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        applicationId "com.example"
        minSdkVersion 17
        targetSdkVersion rootProject.ext.targetSdkVersion
        buildConfigField "String", "BUILD_TIME", "\"" + getDateAndTime() + "\""
        buildConfigField "String", "VERSION_BUILD", "\"" +     project["VERSION_BUILD"] + "\""
        versionCode Integer.parseInt(project.VERSION_CODE)
        versionName project.VERSION_NAME
    }

    signingConfigs {
        debug {
            storeFile file(project.DEBUG_KEYSTORE)
            storePassword project.DEBUG_KEYSTORE_PASSWORD
            keyAlias project.DEBUG_KEYSTORE_ALIAS
            keyPassword project.DEBUG_KEY_PASS
        }

        release {
            storeFile file(project.RELEASE_KEYSTORE)
            storePassword project.RELEASE_KEYSTORE_PASSWORD
            keyAlias project.RELEASE_KEYSTORE_ALIAS
            keyPassword project.RELEASE_KEY_PASS
        }
    }

    sourceSets {
        main {
            res.srcDirs = ['src/main/res/',
                           'src/main/abc']
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            zipAlignEnabled true
            proguardFile getDefaultProguardFile('proguard-android-optimize.txt')
            proguardFile 'proguard-rules.pro'
            signingConfig signingConfigs.release

        }
        debug {
            testCoverageEnabled = true
            debuggable true
            signingConfig signingConfigs.debug
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    testCompile ('junit:junit:4.12')
    testCompile ('org.apache.maven:maven-ant-tasks:2.1.3')
    testCompile ('org.robolectric:robolectric:3.0')
    testCompile ('org.robolectric:shadows-support-v4:3.0')
}

sourceSets {
    unitTest {
        java.srcDirs = ['src/test/java']
        resources.srcDirs = ['src/test/resources']
    }
}

ClassLoader getClassLoader() {
    List urls = sourceSets.test.runtimeClasspath.collect {
        it.toURI().toURL()
    }
    return URLClassLoader.newInstance( urls as URL[] )
}    

/**
 *  Filters out files that have specific annotation
 * @param map - map of things to filter
 * @return - list of acceptable files after filter
 */
List annotationFilter( Map map ) {
   map.prefix = map?.prefix ?: '' // prefix: provide convenience for passing in annotation names

    ClassLoader loader = classLoader
    List result

    // filter with annotations
    if( !map.includes ) {
        result = map?.names
    } else {
        result = []
        map?.names.each { name ->
            Class klass = loader.loadClass( name )
            map?.includes.each { annotationName ->
                String fullName = map.prefix + annotationName
                Class annotation = loader.loadClass( fullName ).asSubclass( Annotation )
                if( !klass.isAnnotationPresent( annotation ) ) {
                    result << name
                }
            }
        }
    }

    if( result?.size() == 0 ) result = [ 'no.tests.to.run' ]
    return result
}

/**
 * Gradle task to run only robolectric tests.
 */
task unitTest( type: Test, description: 'Run all junit tests' ) {

    android.sourceSets.main.java.srcDirs.each { dir ->
        def buildDir = dir.getAbsolutePath().split('/')
        buildDir =  (buildDir[0..(buildDir.length - 4)] + ['build',     'classes', 'debug']).join('/')


        sourceSets.unitTest.compileClasspath += files(buildDir)
        sourceSets.unitTest.runtimeClasspath += files(buildDir)
    }

    testClassesDir = sourceSets.unitTest.output.classesDir
    classpath = sourceSets.unitTest.runtimeClasspath

    doLast {
        println "Doing Last"
        List names = testClassNames()
        List filtered = annotationFilter( names: names, includes:   ['testUtils.RobolectricTest'] )
        println 'Running ' + filtered.size() + ' tests:\n' +     filtered*.toString()*.replaceAll('^','\t').join('\n')

       filter {
            setIncludePatterns( filtered as String[] )
        }
    }
}

回答1:


Not so much Robolectric-specific, but in regards to declaring a custom test task for Android with Gradle, I ran into a lot of trouble with this. As you found, all of the documentation and examples are using the Java plugin, but the Android plugin subverts most of it.

The only solution I found with the Android plugin is to create another build type, which will then result, under the hood, in the Android plugin creating new test tasks for me. Then I can easily modify the tasks.

Here is the relevant setup, which I keep in a <rootProject>/gradle/test.gradle file (This specific example uses Category annotation to filter unit tests into one task and integration tests into a separate task. For information on that part of it see https://github.com/junit-team/junit4/wiki/Categories):

android {

    buildTypes {
        integration
    }

    testOptions {
        unitTests.all {
            useJUnit()

            if (it.name == 'testIntegrationUnitTest') {
                options {
                    excludeCategories 'com.example.categories.UnitTest'
                }
            } else {
                options {
                    excludeCategories 'com.example.categories.IntegrationTest'
                }
            }
        }
    }
}

Then the <module>/build.gradle file applies from this file with apply from: "../gradle/test.gradle"

This causes the android closure from the two files to be merged, results in a new integration build type, which in turn results in a new testIntegrationUnitTest task on the project in addition to the standard testDebugUnitTest and testReleaseUnitTest tasks.

But now, testDebugUnitTest runs only "real" unit tests, while testIntegrationUnitTest runs only integration tests.

Your mileage/implementation may vary. Once you're inside the unitTest.all closure you can do whatever manipulation you need to the options.




回答2:


Custom Gradle Task To Only Run Specific Tests

Building on @alphonzo79 answer, I was able to solve the issue.

The things to know

  • Android gradle omits common testing features in gradle, like exclude and custom sourceSets.
  • Retention and Target was not helpful. Only categories worked.
  • You don't need a new build type, you only need to change the test option when compiling your own tasks.
  • Don't use this - https://github.com/pkainulainen/gradle-examples/blob/master/integration-tests/build.gradle

The complete answer was to create a custom task that changed a flag for the android testOptions to excludeCategories.

LINK

CODE

def integrationTests = false

...

testOptions {
    unitTests.all {
        useJUnit()
        if (integrationTests.toBoolean()) {
            println "Integration Tests Only for " + it.name
            options {
                excludeCategories 'com.example.reactivemvp.categories.UnitTest'
            }
        } else {
            println "Unit Tests Only for " + it.name
            options {
                excludeCategories 'com.example.reactivemvp.categories.IntegrationTest'
            }
        }
    }
}

...

task integrationTest(
    type: Test,
    description: 'Run integration tests only. Pass in \'-Pintegration=true\'',
    dependsOn: ['testDebugUnitTest', 'clean'] ) {

    //Here for task completion, not actually used since sub task of testDebugUnitTest
    testClassesDir = file("src/integrationTest/java/");
    classpath = files("$System.env.ANDROID_HOME/sources/android-18")

    //
    //Turn on integration testing when argument exists and is true
    //
    if (project.hasProperty('integration')) {
        println integration
        if (integration == 'true') {
            integrationTests = true
        }
    }
  }



回答3:


You said Robolectric can't find AndroidManifest.xml when testing multidimensional flavor project on Ubuntu

try to explicitly give the path to your manifest file in the @Config annotation

@Config(constants = BuildConfig.class, manifest = "../<path to>/AndroidManifest.xml")



回答4:


We can use the following configuration to exclude multiple tests by name:

def integrationTests = false
android {
    //...
    testOptions {
        unitTests {
            includeAndroidResources = true
            returnDefaultValues = true
            all {                
                test {
                    filter {
                        if (integrationTests.toBoolean()) {
                            includeTestsMatching "*IntegrationTest"
                        } else {
                            includeTestsMatching "*UnitTest"
                        }
                    }
                }
            }
        }
    }
}

In command line:

gradlew test -->Run only *UnitTest


来源:https://stackoverflow.com/questions/31819127/custom-gradle-test-task-for-android

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!