Using the new Android Build System and TDD
Post #2: michael, 2013-02-28
Today I ported our android build to use the new gradle based build system. As we heavily employ Test Driven Development I also wanted to be able to test drive our application without resorting to test on devices - that proved to be a challenge.
The new android build system
It is still in development and I guess far from being released, nevertheless it seems very promising to us as we build almost all of our Java-based software with gradle. The default setup is very simple, there are only 4 things to remember:
- the android plugin must be on the classpath for the build process
- the project must apply the android plugin
- you have to put the sources in the correct directories
- you have to tell the android plugin where the android sdk is
The following snippet shows an example build.gradle
like the one published in the official documentation:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.3'
}
}
apply plugin: 'android'
android {
compileSdkVersion 15
}
You should also notice that the directory structure is different from a standard android project. Basically you have to put all the sources and resources into src/main
. Device tests have to go into src/instrumentTest
.
Finally create a new file local.properties
and put something like: sdk.dir=/usr/local/android-sdk/r21.1
into it. Please replace the path with your own.
Adding a task for unit tests
The android plugin doesn't have a task for standard unit tests which is quite unfortunate, therefore we have to add that. First we have to add a sourceSet
for our test. Reading the documentation we discover that the sourceSets are configured in the android
section. Naturally you would think you could just add an additional sourceSet
, but the following doesn't work.
android {
sourceSets {
test {
java.srcDir file('src/test/java')
resources.srcDir file('src/test/resources')
}
}
}
First it seems to work, but when you try to use it:
task unitTest(type:Test, dependsOn: assemble) {
description = "run unit tests"
testClassesDir = project.android.sourceSetsContainer.test.output.classesDir
classpath = project.android.sourceSetsContainer.test.runtimeClasspath
}
It just tells you:
Could not find property 'output' on source set test.
In other words, we can find the source set, but we cannot access the directory where the classes are - and therefore cannot run the tests.
Hacky workaround
I wasn't in the mood to give up, probably there is another way to access the correct directories but here is what I came up with:
apply plugin: 'android'
android {
compileSdkVersion 15
}
sourceSets {
unitTest {
java.srcDir file('src/test/java')
resources.srcDir file('src/test/resources')
}
}
configurations {
unitTestCompile.extendsFrom runtime
unitTestRuntime.extendsFrom unitTestCompile
}
dependencies {
unitTestCompile files("$project.buildDir/classes/release")
}
task unitTest(type:Test, dependsOn: assemble) {
description = "run unit tests"
testClassesDir = project.sourceSets.unitTest.output.classesDir
classpath = project.sourceSets.unitTest.runtimeClasspath
}
check.dependsOn unitTest
We define a standard java sourceSet, this works as the android plugin uses the JavaBasePlugin
. In the dependencies section we declare our dependency to the application code - as you might see, this is the hacky part and I will search for a better solution in the next few days. If there is one I will post an update. Last we define our Test
task and declare it to be executed in the check phase.
Robolectric Tests
This gives us unit tests, but it doesn't give us the capabilities to test source code that depends on the android API. Of course that doesn't make a lot of sense therefore we will integrate Robolectric to drive our tests.
First we add additional dependencies to our dependencies section so it looks like this:
repositories {
mavenCentral()
}
dependencies {
unitTestCompile files("$project.buildDir/classes/release")
unitTestCompile 'junit:junit:4.10', 'org.mockito:mockito-core:1.9.0'
unitTestCompile 'com.google.android:android:4.0.1.2'
unitTestCompile 'com.pivotallabs:robolectric:1.2'
configurations.unitTestCompile.exclude group: 'com.google.android.maps'
}
We just add a dependency on the android API and robolectric and exclude the maps API which isn't necessary if you don't use it. Otherwise you just have to download it yourself as google doesn't permit its upload to maven central. Don't forget to declare a repository where gradle can find the dependencies.
Writing the Test
As our project structure is now different from the standard android project structure, we have to tell Robolectric where our AndroidManifest.xml
and the other resources are. To do that we define our own runner:
import com.xtremelabs.robolectric.RobolectricTestRunner;
import org.junit.runners.model.InitializationError;
import java.io.File;
public class HelloWorldRunner extends RobolectricTestRunner {
public HelloWorldRunner(Class testClass) throws InitializationError {
super(testClass, new File("src/main"));
}
}
With this runner we can write a simple test case:
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(HelloWorldRunner.class)
public class HelloWorldTest {
@Test
public void testInstantiation() {
new HelloWorldActivity();
}
}
I know it doesn't look like much, but try to run this testcase without Robolectric and you will see that it is really necessary.
Conclusion
Gradle is a great build system and we could easily extend the android plugin with ordinary unit tests. I think it is the right choice for the new android build tool and I am looking forward to using it.
Download the whole example project.