A Gradle plugin to add unit testing to the Android's plugin. Prepared for Robolectric.
-
Add the plugin to the buildscript's dependencies like this:
buildscript { dependencies { repositories { mavenCentral() } classpath 'com.android.tools.build:gradle:0.14.4' classpath 'com.github.jcandksolutions.gradle:android-unit-test:2.0.2' } }
-
Apply the
android-unit-test
plugin AFTER you declare the Android's plugin and configure it. Like this:apply plugin: 'com.android.application' android { ... } apply plugin: 'android-unit-test'
-
Add dependencies. The plugin adds several configurations. It adds a test configuration
testCompile
, one for each build type (Debug, ...) and one for each flavor declared in the Android extension. For example:testCompile 'junit:junit:4.10' testCompile 'org.robolectric:robolectric:2.3.+' testDebugCompile 'org.debugonly.dependency' testFreeCompile 'Admob.jar'
-
Add tests. The plugin adds several source sets. A master test source set
src/test/java
, a source set for each build type except release, a source set for each flavor, a source set for each combination of flavor and build type, a source set for each flavor group and a source set for each combination of flavor group and build type. For example:- Main tests:
src/test/java/*Test.java
- Debug build type tests:
src/testDebug/java/*Test.java
- Free flavor tests:
src/testFree/java/*Test.java
- FreeBeta grouped flavors (different flavor groups):
src/testFreeBeta/java/*Test.java
- Free flavor & Debug build type:
src/testFreeDebug/java/*Test.java
- FreeBeta grouped flavors & Debug build type:
src/testFreeBetaDebug/java/*Test.java
Warning: All tests must end in
*Test.java
, otherwise, the plugin will not detect them as tests!!! - Main tests:
-
Preferably, Add the applicationId in the
android.defaultConfig
section. This is because theR.java
file is always generated under this package name and Robolectric will try to read the resources from this package name. If you specify a different package name for your flavor, Robolectric would think the R.java class is under this package name. To solve this, the plugin reads the main package name and injects it as a system property so the custom runner can initialize Robolectric correctly. If you don't specify this ID, the plugin will try to read it from the main manifest. For example:android { ... defaultConfig { applicationId "com.example" } }
-
If you are using Robolectric 2.0 to 2.2 (2.3 no longer needs it), you will need a custom Robolectric runner and AndroidManifest classes:
- RobolectricGradleTestRunner:
package com.example; import org.junit.runners.model.InitializationError; import org.robolectric.AndroidManifest; import org.robolectric.AndroidManifestExt; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.res.Fs; public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(final Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(final Config config) { final String manifestProperty = System.getProperty("android.manifest"); if (config.manifest().equals(Config.DEFAULT) && manifestProperty != null) { final String resProperty = System.getProperty("android.resources"); final String assetsProperty = System.getProperty("android.assets"); final String packageProperty = System.getProperty("android.package"); final AndroidManifestExt a = new AndroidManifestExt(Fs.fileFromPath(manifestProperty), Fs.fileFromPath(resProperty), Fs.fileFromPath(assetsProperty)); a.setPackageName(packageProperty); return a; } return super.getAppManifest(config); } }
- AndroidManifestExt:
package org.robolectric; import org.robolectric.res.FsFile; public class AndroidManifestExt extends AndroidManifest { private static final String R = ".R"; private String mPackageName; private boolean isPackageSet; public AndroidManifestExt(final FsFile androidManifestFile, final FsFile resDirectory, final FsFile assetsDirectory) { super(androidManifestFile, resDirectory, assetsDirectory); } @Override public String getRClassName() throws Exception { if (isPackageSet) { parseAndroidManifest(); return mPackageName + R; } return super.getRClassName(); } @Override public String getPackageName() { if (isPackageSet) { parseAndroidManifest(); return mPackageName; } else { return super.getPackageName(); } } public void setPackageName(final String packageName) { mPackageName = packageName; isPackageSet = packageName != null; } }
-
Annotate your tests that need Robolectric with either
@RunWith(RobolectricTestRunner.class)
if using Robolectric 2.3,@RunWith(RobolectricGradleTestRunner.class)
if using Robolectric 2.0 to 2.2, or whatever custom runner you use. -
Run your tests:
- Run
gradlew test
to run the JUnit tests only. - Run
gradlew check
to run both JUnit tests and instrumentation tests. - Run
gradlew testPaidNormalDebug
to run a single variant tests.
Optionally you can use system properties to select a subset of the tests to run:
-Dtest.single
property will set a include pattern for all variants.-DtestPaidNormalDebug
property will set a include pattern only for that specific variant.
For example:
gradlew test -Dtest.single=NormalTest
would run all variants but only the variants with the Normal flavor would find this test and run it.gradlew testPaidNormalDebug -DtestPaidNormalDebug.single=NormalTest
would only run the PaidNormalDebug variant and only the test NormalTest.gradlew testPaidNormalDebug -Dtest.single=NormalTest
would achieve the same of the above one.gradlew testPaidNormalDebug -Dtest.single=BetaTest
would not find any test to run and pass with 0 errors.gradlew test -Dtest.single=NormalTest -DtestPaidNormalDebug.single=PaidTest
would run the NormalTest in all variants that has it and PaidTest only in PaidNormalDebug variant.
Note: Using any of this two flags will override the default pattern of
**/*Test.class
which would mean that your tests will not have to end in*Test.java
to be recognized, so pay attention! - Run
-
Read the results. Gradle generates reports and results in
build/test-report/
andbuild/test-results/
respectively. Each variant will have its independent report. For examplebuild/test-report/freebeta/debug/index.html
. But there will be a merged report with all tests inbuild/test-report/index.html
. -
Optionally, you can use the plugin's extension to configure some options. For example:
```groovy
apply plugin: 'android-unit-test'
androidUnitTest {
testReleaseBuildType true //Run tests for all the build types including non-debuggable (like the Release build type). Only works for Application projects, not Library projects.
downloadDependenciesSources false //Download the sources.jar for the production dependencies. `true` by default.
downloadDependenciesJavadoc true //Download the javadoc.jar for the production dependencies. `false` by default.
downloadTestDependenciesSources false //Download the sources.jar for the test dependencies. `true` by default.
downloadTestDependenciesJavadoc true //Download the javadoc.jar for the test dependencies. `false` by default.
}
```
- Gradle 2.10 or superior.
- Android's Gradle Plugin 0.14.0 or superior.
- An Android app or library that builds with Gradle.
To run the sample app you'll need to first install the SNAPSHOT version of this plugin. This is easily done with a simple instruction:
cd /pathToProject
gradlew install
After the installation of the SNAPSHOT version you can run the example with another simple command:
cd /pathToProject/example
../gradlew check
As you can notice, the example is run with the gradle wrapper of the main project. Hence the need of ../
(..\
on Windows) to run the wrapper inside the example dir.
The wrapper should download Gradle 1.12. The example depends on Android's plugin version 0.12.+
which it will also download. Finally the example needs Android platform 20 and build tools 20. If you don't have them, you can either download them from the SDK Manager, or you can modify the build.gradle file and put the platform and build tools you use.
There is an Intellij plugin to integrate with Android Studio. Otherwise, there are two hacks that will make Android Studio recognize the source paths, dependencies and the second one will also allow to use the JUnit integrated tests (Keep in mind that the integrated JUnit tests are not the same tests executed by gradle so results may vary):
Plugin:
Scripted:
Manual:
-
Declare new dependencies to your project in the
.idea/libraries
directory, for example robolectric_2_3_SNAPSHOT_jar_with_dependencies.xml:<component name="libraryTable"> <library name="robolectric-2.3-SNAPSHOT-jar-with-dependencies"> <CLASSES> <root url="jar://$MAVEN_REPOSITORY$/org/robolectric/robolectric/2.3-SNAPSHOT/robolectric-2.3-SNAPSHOT-jar-with-dependencies.jar!/" /> </CLASSES> <JAVADOC /> <SOURCES> <root url="jar://$MAVEN_REPOSITORY$/org/robolectric/robolectric/2.3-SNAPSHOT/robolectric-2.3-SNAPSHOT-jar-with-dependencies.jar!/" /> <root url="jar://$MAVEN_REPOSITORY$/org/robolectric/robolectric/2.3-SNAPSHOT/robolectric-2.3-SNAPSHOT-sources.jar!/" /> </SOURCES> </library> </component>
-
Declare a false dependency in the same folder to the Android SDK to cheat the JUnit tests into reading the android SDK after the JDK, for example android.xml:
<component name="libraryTable"> <library name="android"> <CLASSES> <root url="jar://$APPLICATION_HOME_DIR$/sdk/platforms/android-20/android.jar!/" /> </CLASSES> <JAVADOC /> <SOURCES /> </library> </component>
-
Open the
.iml
file of the project that uses the plugin. -
Add each source directory that you need inside the content tag. For example:
<content url="file://$MODULE_DIR$"> ... <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> ... </content>
-
Register the dependencies for the test sourceSets inside the component tag like this (Notice the false android library dependency):
<component name="NewModuleRootManager" inherit-compiler-output="false"> <content url="file://$MODULE_DIR$"> ... </content> ... <orderEntry type="library" scope="TEST" name="fest-android-1.0.7" level="project" /> <orderEntry type="library" scope="TEST" name="fest-assert-core-2.0M10" level="project" /> <orderEntry type="library" scope="TEST" name="junit-4.10" level="project" /> <orderEntry type="library" scope="TEST" name="mockito-core-1.9.5" level="project" /> <orderEntry type="library" scope="TEST" name="robolectric-2.3-SNAPSHOT-jar-with-dependencies" level="project" /> <orderEntry type="library" scope="TEST" name="android" level="project" /> </component>
-
Modify the test output so it knows where to find the tests. Keep in mind that multiple flavors will have multiple outputs and you'll probably have to change this frequently. Assuming no flavor, add this under
component name="NewModuleRootManager"
:<output-test url="file://$MODULE_DIR$/build/test-classes/debug" />
-
Be sure to have the
.iml
file under version control since every time you open Android Studio it will erase your changes. Having it under version control will allow you to simply revert the changes that Android Studio does instead of manually modifying the file each time. -
Open the Run/Debug Configurations dialog.
-
Create a new Gradle Configuration. Name it however you want. In
Gradle project:
select the root project path. In tasks, add all the assemble tasks of any project dependency like a library. and then theClasses
task depending on the flavors you have. For example:Gradle Project: C:/Sources/Android Tasks: androidLibrary1:assemble androidLibrary2:assemble androidApp:testDebugClasses (androidApp:testFlavor1DebugClasses if you have flavors)
-
Create a new JUnit Configuration: in
Directory:
point to the source folder of the tests (you may have to create one configuration for each flavor). InWorking directory:
set to root project dir. InUse classpath of module:
Choose the app module. ActivateUse alternate JRE:
and select a standard JDK or JRE. InBefore launch:
remove the build task and add the Gradle configuration you made in the previous step. -
You can run (even debug) your tests in two ways. The easiest way is to select the JUnit configuration made in the previous step and click
Run
(orDebug
) button, do notice however that this tests are not being executed throw gradle, they are using the same class files however (That is why we executed the testDebugClasses task). The other way is to execute the actual gradle test task that the plugin makes. From the Gradle tab. Just select thecheck
task, right click it, and selectRun 'Project [check]'
(orDebug 'Project [check]'
). If you are debugging tests in this way, you will have to uncheck theUse in-process build
in the Compiler > Gradle settings. Finally, you can also use the terminal tab to execute directly the task but this will not let you debug (unless you use yet another hack to attach the debugger which I will not cover here).
-
Q: Why is there a
build/test-resources/VariantFullName/res/
directory that has exactly the same asbuild/res/all/variantflavors/buildtype/
?A: Robolectric 2.0 to 2.1 had a bug that made paths with a keyword in them (like
menu
,layout
, etc.) to parse incorrectly. In Robolectric 2.2 there was a patch to fix this that forced the resource path to finish withres/
since Eclipse projects have that structure. The problem is that gradle projects don't have that structure. Because of this the plugin has to copy all the merged resources to a new folder that has ares/
directory just before the actual resources. This restriction has since been fixed in Robolectric 2.3-SNAPSHOT, but to stay backward compatible, the plugin will have to keep copying the resources tobuild/test-resources/
.
- Robolectric team for making an awesome test framework.
- Square's plugin that inspired this plugin: https://github.com/square/gradle-android-test-plugin.