Skip to content

Instantly share code, notes, and snippets.

@avbk
Last active October 14, 2017 00:16
Show Gist options
  • Save avbk/d82c44ed8bef5782dbe5 to your computer and use it in GitHub Desktop.
Save avbk/d82c44ed8bef5782dbe5 to your computer and use it in GitHub Desktop.
Testing with Robolectric within Android Studio

Testing with Robolectric within Android Studio

Prerequisites

0. Setup a project

Setup a new Android project with Android Studio. If you already have done this you can skip this step.

You find a description of the process here. From this point I'm assuming the following project structure:

robo
	|- app
    |	|- build
    |	|- libs
    |	|- src
    |	|- build.gradle
    |- build.gradle
    |- settings.gradle

/* ommitted some files due to laziness */

If not noted elsewise every path in this document will be relative to robo

1. Setup Robolectric

Add the robolectric classpath to build.gradle:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:0.12.2"
        classpath "org.robolectric:robolectric-gradle-plugin:0.11.+" // robolectric classpath
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Add Robolectric dependencies to app/build.gradle:

apply plugin: "com.android.application"
apply plugin: "robolectric" // load robolectric plugin

android {
    compileSdkVersion 20
    buildToolsVersion "20.0.0"

    defaultConfig {
        applicationId "de.neusta.ms.avbk.example.robo"
        minSdkVersion 14
        targetSdkVersion 20
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        debug {
            debuggable true
        }
        release {
            runProguard false
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }
}

// setup robolectric to use only files below a folder with name "robolectric"
robolectric {
    include "**/robolectric/**/*Test.class"
}

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

     // use robolectric when testing
    androidTestCompile("org.robolectric:robolectric:2.3") {
        exclude module: "commons-logging"
        exclude module: "httpclient"
    }
    androidTestCompile "junit:junit:4.11" // use junit 4 for testing
}

2. Create a Robolectric Test

Create a new directory for your Robolectric Tests in app/src/androidTest/java/ according to your applicationId. So for this example, where the applicationId is de.neusta.ms.avbk.example.robo, I put my Robolectric test files to app/src/androidTest/java/de/neusta/ms/avbk/example/robo/tests/robolectric/.

Create a first Robolectric test in app/src/androidTest/java/de/neusta/ms/avbk/example/robo/tests/robolectric/SampleTest.java:

package de.neusta.ms.avbk.example.robo.tests.robolectric;

import android.app.Activity;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import de.neusta.ms.avbk.example.robo.MyActivity;
import de.neusta.ms.avbk.example.robo.R;

import static org.junit.Assert.assertNotNull;

@RunWith(RobolectricTestRunner.class)
@Config(emulateSdk = 18)
public class SampleTest {

    @Test
    public void testRobolectricWorks() throws Exception {
        Activity activity = Robolectric.buildActivity(MyActivity.class).create().get();

        assertNotNull(activity);
        assertNotNull(activity.findViewById(R.id.example_textview));
    }
}

3. Run the test with gradle from the shell

Open up a terminal (or command line for those who have to use Windows), navigate to the app module of your project and run gradle

avbk@alex-laptop ~ % cd projects/robo/app 
avbk@alex-laptop ~/projects/robo/app % gradle test
Creating properties on demand (a.k.a. dynamic properties) has been deprecated and is scheduled to be removed in Gradle 2.0. Please read http://gradle.org/docs/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html for information on the replacement for dynamic properties.
Deprecated dynamic property: "destinationDir" on "task ':app:testDebugClasses'", value: "/home/avbk/projects/ro...".
:app:preBuild
:app:preDebugBuild
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl UP-TO-DATE
:app:compileDebugRenderscript UP-TO-DATE
:app:generateDebugBuildConfig UP-TO-DATE
:app:generateDebugAssets UP-TO-DATE
:app:mergeDebugAssets UP-TO-DATE
:app:generateDebugResValues UP-TO-DATE
:app:generateDebugResources UP-TO-DATE
:app:mergeDebugResources UP-TO-DATE
:app:processDebugManifest UP-TO-DATE
:app:processDebugResources UP-TO-DATE
:app:generateDebugSources UP-TO-DATE
:app:compileDebugJava UP-TO-DATE
:app:compileTestDebugJava
:app:processTestDebugResources UP-TO-DATE
:app:testDebugClasses
:app:testDebug
:app:test

BUILD SUCCESSFUL

Total time: 7.51 secs

If anything fails at this point, check Google and StackOverflow for help ;-)

4 Setup Android Studio

4.1 Let Gradle include test classes

Edit your app/build.gradle to add a special task to include build/test-classes into the classpath if needded:

/** omitted lines for better readability **/ 

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

    androidTestCompile("org.robolectric:robolectric:2.3") {
        exclude module: "commons-logging"
        exclude module: "httpclient"
    }
    androidTestCompile "junit:junit:4.11"
}

// tell gradle to include robolectric test classes
task copyTestClasses(type: Copy) {
    from "build/test-classes"
    into "build/intermediates/classes/debug"
}

(if you want to test a different flavour or buildtype, change /debug in this path accordingly)

4.2 Setup Task to compile and include test classes

In Android Studio you need to add a new Run-Configuration.

  1. Click on Run -> Edit Configurations
  2. In the dialog press the + Symbol and select Gradle
  3. Setup the configuration as to following:
  • Gradle Project: :robo:app
  • Tasks: testClasses copyTestClasses

Save this configuration by pressing OK.

4.3 Setup JUnit Run-Configuration

In Android Studio you need to add a new Run-Configuration.

  1. Click on Run -> Edit Configurations
  2. In the dialog press the + Symbol and select JUnit
  3. Setup the configuration as to following:
  • Name: Robolectric
  • Test kind: All in package
  • Package: de.neusta.ms.avbk.example.robo.tests.robolectric
  • Search for tests: In single module
  • VM Options: -ea -Dandroid.assets=build/intermediates/assets/debug -Dandroid.manifest=build/intermediates/manifests/debug/AndroidManifest.xml -Dandroid.resources=build/intermediates/res/debug (if you want to test a different flavour or buildtype, change debug in those paths accordingly)
  • Working Directory: $MODULE_DIR$
  • Use classpath of module: app
  • Check Use alternate JRE and select your default Java VM, e.g. /usr/lib/jvm/java-1.7.0-openjdk-i386
  • Under Before Launch press + and add Build test classes by selecting Run Another Configuration

Save this configuration by pressing OK.

It should look like this screenshot:

This configuration will compile and run Robolectric with your default JVM and setup the assets, resources and manifest correctly. But since it will run on the wrong JVM, none of the Android Classes are found, which leads to the following error message:

java.lang.NoClassDefFoundError: android/R
	at org.robolectric.bytecode.Setup.<clinit>(Setup.java:39)
	at org.robolectric.RobolectricTestRunner.createSetup(RobolectricTestRunner.java:137)
	at org.robolectric.RobolectricTestRunner.createSdkEnvironment(RobolectricTestRunner.java:114)
    ...

Obviously you need to include the android.jar, but if you would do so you will fail with the famous stub-exception:

!!! 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)
    ...

This is due to the fact that android has its own version of JUnit included (as stubs) and Android Studio will select its TestRunner instead of the TestRunner of Robolectric. I read about hacks to reorder your *.iml so that Robolectric's TestRunner will be selected by default. But I don't like this approach, since the *.iml files will be recreated whenever Android Studio syncs the gradle files.

So I came up with another solution that might look even more like an evil hack but imho is more robust.

4.4 Include a modified version of android.jar

The basic idea is to use the android.jar of API-Level 18 without its JUnit capabilities. This can be achieved quite easily.

Open up a terminal (sorry Windows-Users, you have to figure out a more GUI-ish way I guess ;-) ) and enter the following:

avbk@alex-laptop ~ % cd projects/robo/app 
avbk@alex-laptop ~/projects/robo/app % mkdir testlibs
avbk@alex-laptop ~/projects/robo/app % cp $ANDROID_HOME/platforms/android-18/android.jar testlibs/android-18-stripped.jar
avbk@alex-laptop ~/projects/robo/app % zip --delete testlibs/android-18-stripped.jar junit*
deleting: junit/
deleting: junit/framework/
deleting: junit/framework/TestCase.class
deleting: junit/framework/Test.class
deleting: junit/framework/AssertionFailedError.class
deleting: junit/framework/ComparisonFailure.class
deleting: junit/framework/TestFailure.class
deleting: junit/framework/Assert.class
deleting: junit/framework/TestSuite.class
deleting: junit/framework/TestListener.class
deleting: junit/framework/Protectable.class
deleting: junit/framework/TestResult.class
deleting: junit/runner/
deleting: junit/runner/Version.class
deleting: junit/runner/TestSuiteLoader.class
deleting: junit/runner/BaseTestRunner.class

(note that $ANDROID_HOME points to the installation path of my Android SDK)

After JUnit is removed from the jar, you only need to tell gradle to use it when testing. Edit your app/build.gradle:

/** omitted lines for better readability **/ 

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

    androidTestCompile("org.robolectric:robolectric:2.3") {
        exclude module: "commons-logging"
        exclude module: "httpclient"
    }
    androidTestCompile "junit:junit:4.11"
    
    // add the stripped version of android.jar
    androidTestCompile files("testlibs/android-18-stripped.jar")
}

task copyTestClasses(type: Copy) {
    from "build/test-classes"
    into "build/intermediates/classes/debug"
}

Now you should be able to run and debug your tests from Android Studio by running the Robolectric-Configuration (infrequently this method fails to start up JUnit correctly and ends with an exception of a missing JUnit file. But on the second run it worked for me TM ;-) )

Got inspired by http://blog.blundell-apps.com/how-to-run-robolectric-junit-tests-in-android-studio/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment