From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
UPDATE: This post has been superseded by a newer version here.
In May of 2014, we made the switch to Android Studio as our preferred Android IDE, migrating away from Eclipse in our Android bootcamps and book.
As we have started new app development projects for clients, we transitioned away from IntelliJ, our previous IDE of choice. This shift has allowed us to build a uniform skillset, enhancing both our teaching and consulting. However, we still had trouble integrating some of the tools and techniques that made our past projects so successful. Android Studio recently moved out of beta, so now is a good time to share about our successes so far.
In our consulting projects, we’ve benefited from building a solid set of unit tests with Robolectric, for a few reasons:
We’ve spent a lot of brainpower thinking about different design architectures and patterns. All of the many patterns we discuss rely on implementing some separation of concerns. While we haven’t come to a decision about which architecture is best for us—and this is always subject to change—we realized that Android Studio modules would help us achieve this separation.
We’ve put together a set of best practices for setting up a project that uses Android Studio, Gradle and Robolectric.
Our consulting projects are usually architected in multiple layers, with the model layer coded in pure Java. This means that there’s no dependency on the Android framework. Breaking this layer out into its own Android Studio module provides us with several benefits:
Setting up a Java module in Android Studio is straightforward. Under “Project Structure,” click “+” and then select “Java Library.” Android Studio will generate the folder structure for this module and provide us with a separate build.gradle
file. For this “core” module, the build.gradle
file is relatively simple:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.+'
}
To include the core
module in our app
module, we add it as a local dependency:
apply plugin: 'com.android.application'
android {...}
dependencies {
compile project(':core')
}
One caveat worth mentioning: Any test classes of the core
module in the /core/src/test directory will not be compiled into the .jar, which satisfies the dependency in the app
module. If we need the core
module to provide any test helpers or test doubles, we will have to add them as a separate dependency.
Robolectric and Android Studio/Gradle do not work together out of the box. We found two different solutions to integrate them. Both solutions work, but we found one to be superior.
The first solution is to use Robolectric’s provided Gradle plugin. However, there is a drawback: Robolectric and Android Studio each utilize a different version of Junit. This causes a mismatch when compiling:
!!! JUnit version 3.8 or later expected:
java.lang.RuntimeException: Stub!
at junit.runner.BaseTestRunner.<init>(BaseTestRunner.java:5)
at junit.textui.TestRunner.<init>(TestRunner.java:54)
at junit.textui.TestRunner.<init>(TestRunner.java:48)
at junit.textui.TestRunner.<init>(TestRunner.java:41)
Android Studio builds the dependency .iml file for us, so there is no way to prioritize these dependencies by manually setting the order of the entries. The Robolectric documentation says:
Android Studio aggressively re-writes your dependencies list (your .iml file) and subverts the technique used above to get the Android SDK to the bottom of the classpath. You will get the dreaded Stub! exception every time you re-open the project (and possibly more often). For this reason we currently recommend IntelliJ; we hope this can be solved in the future.
This hangup doesn’t entirely prevent us from using Robolectric with Gradle. We just lose the ability to rely on the IDE to speed up running tests and debugging. We are forced to use Gradle from the command line. We get very poor information on failing tests. Furthermore, we lose the ability to debug tests and set breakpoints.
Luckily, another solution exists. JC&K Solutions has written a Gradle plugin to integrate with Robolectric, and Evan Tatarka has written an Android Studio plugin to integrate the JC & K Gradle plugin with Android Studio. This solution allows us to run Robolectric unit tests within Android Studio.
To start, we need to include the JC&K Gradle plugin to our root build.gradle
file:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.github.jcandksolutions.gradle:android-unit-test:2.1.1'
}
}
allprojects {
repositories {
jcenter()
}
}
Next, we need to apply the Gradle plugin in our app’s build.gradle
file:
apply plugin: 'com.android.application'
android {...}
apply plugin: 'android-unit-test'
dependencies {
// core android studio module
compile project(':core')
// testing
testCompile 'org.robolectric:robolectric:2.4'
testCompile 'junit:junit:4.+'
}
Then we need to install the Android Studio plugin. From within Android Studio, navigate to Settings -> Plugins -> Browse Repositories… and search for “Android Studio Unit Test.”
Now is a good time to talk about some Gradle parameters that have been mismatched or left unexplained in other tutorials I’ve seen. The directory where our tests live is called a source set. When we create a new project in Android Studio, the IDE defaults to naming this directory androidTest
. See the user guide for more details. However, the Android Studio Unit Test plugin will be looking for our tests in the test
source set. To address this, we could choose to create an alias to remap this source set by adding to our app’s build.gradle
file:
apply plugin: 'com.android.application'
android {...}
apply plugin: 'android-unit-test'
sourceSets {
androidTest.setRoot('src/test')
}
dependencies {...}
However, it is easier, and clearer, to instead rename the androidTest source set (i.e., rename the app/src/androidTest
directory to app/src/test
).
I have also seen several mismatches when naming the Gradle dependency configurations inside the dependencies
block. The Java Gradle plugin is looking for a testCompile
configuration. All test-related dependencies should be applied to that configuration. There is no need for additional configuration dependencies for androidTestCompile
or instrumentTestCompile
.
apply plugin: 'com.android.application'
android {...}
apply plugin: 'android-unit-test'
dependencies {
...
// testing
testCompile 'org.robolectric:robolectric:2.4'
testCompile 'junit:junit:4.+'
// these aren’t getting used
androidTestCompile 'some.other.library'
instrumentTestCompile 'additional.library'
}
As of Android Studio 0.8.9, the test classes are no longer compiled as part of the assembleDebug
task. We have to manually compile them by setting a task dependency. We accomplish this by adding to our app’s build.gradle
file:
apply plugin: 'com.android.application'
android {...}
apply plugin: 'android-unit-test'
afterEvaluate {
tasks.findByName("assembleDebug").dependsOn("testDebugClasses")
}
dependencies {...}
For more information, check out this GitHub issue.
The last thing to do is configure Robolectric to properly locate our app’s AndroidManifest.xml
file. This linking doesn’t happen by default. There are two ways to set this up. The first way is to annotate each test class, hard-linking the test to the manifest:
@RunWith(RobolectricTestRunner.class)
@Config(manifest="./src/main/AndroidManifest.xml")
public class MyActivityTest {
...
}
The second option is to subclass RobolectricTestRunner
to point to the manifest.
public class CustomTestRunner extends RobolectricTestRunner {
public CustomTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
protected AndroidManifest getAppManifest(Config config) {
String appRoot = "../path/to/app/src/main/";
String manifestPath = appRoot + "AndroidManifest.xml";
String resDir = appRoot + "res";
String assetsDir = appRoot + "assets";
AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath),
Fs.fileFromPath(resDir),
Fs.fileFromPath(assetsDir));
manifest.setPackageName("com.my.package.name");
// Robolectric is already going to look in the 'app' dir ...
// so no need to add to package name
return manifest;
}
}
And then use this CustomTestRunner
for each test class. You’ll note that because we use a custom test runner, we have to manually tell Robolectric which API to emulate. We are currently limited to API levels 16, 17 or 18.
@Config(emulateSdk = 18)
@RunWith(CustomTestRunner.class)
public class MyActivityTest {
...
}
I will leave it up to you to decide which of these methods is preferable.
Things are getting good, and looking better now. We have multiple Android Studio modules compiled into a single project. We have Gradle building our entire project for us. We have our Robolectric unit tests running smoothly, all within the IDE. Now it’s time to get down to business and build another sweet Android application!
I’d like to thank Josh Skeen and Ross Hambrick for their help with this post. Check out Josh’s sample repository for setting up Gradle + Android Studio + Robolectric.
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...