donderdag 27 februari 2014

Robolectric plugin - Fix the plugin-flaw on a Windows Environment

In the previous blogpost we worked with the Robolectric plugin (developed by Novoda.com) for starting our Robolectric tests from Gradle. This neat little plugin unfortunealty has a flaw which prohibits it from working correctly if you are developing on a Windows environment (or you switch to Mac or Linux, just an idea...).

The problem with the current plugin is that it does not escape the File.Seperator correctly, and although a merge-request is pending on their Git repository, they haven't fixed this to a new stable or snapshot release.

The code of the current (0.0.1 - SNAPSHOT) currently states:
     def buildDir = dir.getAbsolutePath().split(File.separator)
Where it should be
      def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))

Fortuneatly enough, the source code is publicly available as a Groovy based plugin. For ease-of-use (and my lack of understanding on how to compile a plugin :) ), I've posted my Gradle configuration below.

Next to having your normal buildscript and android configuration, you will need to :
  • Import two additional classes, being Callable and Pattern
  • Define the plugin: robolectric
  • Paste-in the code that starts with "class robolectric implements Plugin


Note
With the release of Android Studio 0.5.0, some upgrading of your Gradle build file is required (using the Gradle plugin version 0.9.+). In the dependencies change the instrumentTestCompile command to androidTestCompile. If you'll have exceptions during the Project Gradle Sync, make sure to restart your IDE and invalidate the caches (File > Invalidate Caches/Restart). I've update the build file in the provided examples.

The code

//--- import additional classes ---
import java.util.concurrent.Callable
import java.util.regex.Pattern
//--- end import ---

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
        classpath 'com.novoda.gradle:robolectric-plugin:0.0.1-SNAPSHOT'
    }
}

apply plugin: 'android'
//--- use the new plugin robolectric, which is the classname of the plugin
apply plugin: robolectric

android {
    compileSdkVersion 19
    buildToolsVersion '19.0.1'

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }

    sourceSets {
        //the tests are still stored in /src/test
        instrumentTest.setRoot('src/test')
    }

    buildTypes {
    }
}


dependencies {
    compile 'com.android.support:appcompat-v7:+'
    
    androidTestCompile 'org.robolectric:robolectric:2.+'
    androidTestCompile 'junit:junit:4.+'
}

//ignore classes that do not have the extension Test in their classname
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class"
}
/**
 * -----------------------------------------------------------
 * ROBOLECTRIC PLUGIN CLAS
 * -----------------------------------------------------------
 */
class robolectric implements Plugin<Project> {

    public static final String ANDROID_PLUGIN_NAME = "android";
    public static final String ANDROID_LIBRARY_PLUGIN_NAME = "android-library";

    public static final String COMPILE_CONFIGURATION_NAME = "compile";
    public static final String TEST_COMPILE_CONFIGURATION_NAME = "robolectricTestCompile";
    public static final String RUNTIME_CONFIGURATION_NAME = "runtime";
    public static final String TEST_RUNTIME_CONFIGURATION_NAME = "robolectricTestRuntime";

    public static final String ROBOLECTRIC_SOURCE_SET_NAME = "robolectric";
    public static final String ROBOLECTRIC_CONFIGURATION_NAME = "robolectric";
    public static final String ROBOLECTRIC_TASK_NAME = "robolectric";

    void apply(Project project) {
        project.getPlugins().apply(JavaBasePlugin.class);
        ensureValidProject(project);

        JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class);
        configureConfigurations(project);
        configureSourceSets(javaConvention);

        configureTest(project, javaConvention);

        project.afterEvaluate {
            configureAndroidDependency(project, javaConvention)
        }
    }

    def configureAndroidDependency(Project project, JavaPluginConvention pluginConvention) {
        SourceSet robolectric = pluginConvention.getSourceSets().findByName(ROBOLECTRIC_SOURCE_SET_NAME);

        (getAndroidPlugin(project)).mainSourceSet.java.srcDirs.each { dir ->
            //Fault: def buildDir = dir.getAbsolutePath().split(File.separator)
            def buildDir = dir.getAbsolutePath().split(Pattern.quote(File.separator))
            buildDir = (buildDir[0..(buildDir.length - 4)] + ['build', 'classes', 'debug']).join(File.separator)
            robolectric.compileClasspath += project.files(buildDir)
            robolectric.runtimeClasspath += project.files(buildDir)
        }

        getAndroidPlugin(project).variantDataList.each {
            it.variantDependency.getJarDependencies().each {
                robolectric.compileClasspath += project.files(it.jarFile)
                robolectric.runtimeClasspath += project.files(it.jarFile)
            }
        }

        // AAR files
        getAndroidPlugin(project).prepareTaskMap.each {
            robolectric.compileClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
            robolectric.runtimeClasspath += project.fileTree(dir: it.value.explodedDir, include: '*.jar')
        }

        // Default Android jar
        getAndroidPlugin(project).getRuntimeJarList().each {
            robolectric.compileClasspath += project.files(it)
            robolectric.runtimeClasspath += project.files(it)
        }

        robolectric.runtimeClasspath = robolectric.runtimeClasspath.filter {
            it
            true
        }
    }

    private void ensureValidProject(Project project) {
        boolean isAndroidProject = project.getPlugins().hasPlugin(ANDROID_PLUGIN_NAME);
        boolean isAndroidLibProject = project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        if (!(isAndroidLibProject | isAndroidProject)) {
            throw new RuntimeException("Not a valid Android project");
        }
    }

    void configureConfigurations(Project project) {
        ConfigurationContainer configurations = project.getConfigurations();
        Configuration compileConfiguration = configurations.getByName(COMPILE_CONFIGURATION_NAME);
        Configuration robolectric = configurations.create(ROBOLECTRIC_CONFIGURATION_NAME);
        robolectric.extendsFrom(compileConfiguration);
    }

    private void configureSourceSets(final JavaPluginConvention pluginConvention) {
        final Project project = pluginConvention.getProject();

        SourceSet robolectric = pluginConvention.getSourceSets().create(ROBOLECTRIC_SOURCE_SET_NAME);

        robolectric.java.srcDir project.file('src/test/java')
        robolectric.compileClasspath += project.configurations.robolectric
        robolectric.runtimeClasspath += robolectric.compileClasspath
    }

    private void configureTest(final Project project, final JavaPluginConvention pluginConvention) {
        project.getTasks().withType(Test.class, new Action<Test>() {
            public void execute(final Test test) {
                test.workingDir 'src/main'
                test.getConventionMapping().map("testClassesDir", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getOutput().getClassesDir();
                    }
                });

                test.getConventionMapping().map("classpath", new Callable<Object>() {
                    public Object call() throws Exception {
                        return pluginConvention.getSourceSets().getByName("robolectric").getRuntimeClasspath();
                    }
                });

                test.getConventionMapping().map("testSrcDirs", new Callable<Object>() {
                    public Object call() throws Exception {
                        return new ArrayList<File>(pluginConvention.getSourceSets().getByName("robolectric").getJava().getSrcDirs());
                    }
                });
            }
        });

        Test test = project.getTasks().create(ROBOLECTRIC_TASK_NAME, Test.class);
        project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(test);
        test.setDescription("Runs the unit tests using robolectric.");
        test.setGroup(JavaBasePlugin.VERIFICATION_GROUP);

        test.dependsOn(project.getTasks().findByName('robolectricClasses'))
        test.dependsOn(project.getTasks().findByName('assemble'))
    }

    private Plugin getAndroidPlugin(Project project) {
        if (project.getPlugins().hasPlugin(ANDROID_LIBRARY_PLUGIN_NAME)) {
            return  project.getPlugins().findPlugin(ANDROID_LIBRARY_PLUGIN_NAME);
        }
        return project.getPlugins().findPlugin(ANDROID_PLUGIN_NAME);
    }

}
//--- END CLASS DEFINITION --

woensdag 12 februari 2014

Robolectric - BroadcastReceiver and IntentService testing

One of the added values on mobile apps is the ability to send Push notifications from a back-end application to a specific device (i.e. Push message) or multiple devices (i.e. Broadcast message).

When implementing a form of Push or Broadcast messaging, you generally require two components: a BroadcastReceiver and an IntentService, which we kindly submit to rigorous unit testing.

Testing the BroadcastReceiver

In this example, we just created a BroadcastReceiver that dispatches the intent to the IntentService for further processing.

MyBroadcastReceiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import be.acuzio.mrta.service.MyBroadcastIntentService;


public class MyBroadcastReceiver extends BroadcastReceiver {
    private final static String TAG = MyBroadcastReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive was triggered");

        Intent service = new Intent(context, MyBroadcastIntentService.class);
        service.putExtra("ACTION", intent.getStringExtra("PERFORM"));

        //start the service which needs to handle the intent
        context.startService(service);
    }
}

In the application manifest we defined the receiver to listen to incoming GCM messages, but you might even have BroadcastReceivers listening to local broadcast intents.

ApplicationManifest

      
        ...
        <receiver android:name=".receiver.MyBroadcastReceiver" 
                     android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="be.acuzio.mrta" />
            </intent-filter>
        </receiver>
        ...


MyBroadcastReceiverTest

  
package be.acuzio.mrta.test.receiver;

import android.content.BroadcastReceiver;
import android.content.Intent;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLog;

import java.util.List;

import be.acuzio.mrta.receiver.MyBroadcastReceiver;
import be.acuzio.mrta.service.MyBroadcastIntentService;

/**
 * Created by vandekr on 12/02/14.
 */

@RunWith(RobolectricTestRunner.class)
public class MyBroadcastReceiverTest {
    static {
        // redirect the Log.x output to stdout. Stdout will be recorded in the test result report
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    /**
     * Let's first test if the BroadcastReceiver, which was defined in the manifest, is correctly
     * load in our tests
     */
    @Test
    public void testBroadcastReceiverRegistered() {
        List<ShadowApplication.Wrapper> registeredReceivers = Robolectric.getShadowApplication().getRegisteredReceivers();

        Assert.assertFalse(registeredReceivers.isEmpty());

        boolean receiverFound = false;
        for (ShadowApplication.Wrapper wrapper : registeredReceivers) {
            if (!receiverFound)
                receiverFound = MyBroadcastReceiver.class.getSimpleName().equals(
                                         wrapper.broadcastReceiver.getClass().getSimpleName());
        }

        Assert.assertTrue(receiverFound); //will be false if not found
    }

    @Test
    public void testIntentHandling() {
    /** TEST 1
         ----------
         We defined the Broadcast receiver with a certain action, so we should check if we have
         receivers listening to the defined action
         */
        Intent intent = new Intent("com.google.android.c2dm.intent.RECEIVE");

        ShadowApplication shadowApplication = Robolectric.getShadowApplication();
        Assert.assertTrue(shadowApplication.hasReceiverForIntent(intent));

        /**
         * TEST 2
         * ----------
         * Lets be sure that we only have a single receiver assigned for this intent
         */
        List<broadcastreceiver> receiversForIntent = shadowApplication.getReceiversForIntent(intent);

        Assert.assertEquals("Expected one broadcast receiver", 1, receiversForIntent.size());

        /**
         * TEST 3
         * ----------
         * Fetch the Broadcast receiver and cast it to the correct class.
         * Next call the "onReceive" method and check if the MyBroadcastIntentService was started
         */
        MyBroadcastReceiver receiver = (MyBroadcastReceiver) receiversForIntent.get(0);
        receiver.onReceive(Robolectric.getShadowApplication().getApplicationContext(), intent);

        Intent serviceIntent = Robolectric.getShadowApplication().peekNextStartedService();
        Assert.assertEquals("Expected the MyBroadcast service to be invoked",
                MyBroadcastIntentService.class.getCanonicalName(),
                serviceIntent.getComponent().getClassName());

    }
}


Testing the IntentService

Testing IntentServices is a somewhat more complicated matter. The current implementation of Robolectric does not invoke the "onHandleIntent", ergo nothing happens. A way to fix this, is to stub or mock your IntentService class in your tests and override the "onHandleIntent(Intent intent)" method to broaden its scope from protected to public.

Because of the introduction of Mock objects in our project, we need to update the build.gradle file to exclude all files that are not named "Test" from the TestRunner. Add  this snippet to the bottom of your build.gradle file.

// prevent the "superClassName is empty" error for classes not annotated as tests
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class" // whatever Ant pattern matches your test class files
}

As mentioned in the comment, if you do not add this snippet, you will get these nasty "superClassName is empty" exceptions, which can give you a true run for your money.

MyBroadcastIntentService

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import be.acuzio.mrta.MainActivity;
import be.acuzio.mrta.R;

public class MyBroadcastIntentService extends IntentService {
    private final static String TAG = MyBroadcastIntentService.class.getSimpleName();
    public final static int NOTIFICATION_ID = 335446435;
    public final static String NOTIFICATION_TAG = "BNTAG_ACTION";

    public MyBroadcastIntentService() {
        super(MyBroadcastIntentService.class.getSimpleName());
        Log.d(TAG, "Creating new instance of MyBroadcastIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent was called");

        Bundle extras = intent.getExtras();

        if (extras != null && !extras.isEmpty()) {  // has effect of unparcelling Bundle
            Log.d(TAG, "Extras were found");

            String action = intent.getStringExtra("ACTION");

            this.sendNotification(action);
        }
    }

    private void sendNotification(String action) {
        Log.d(TAG, "Sending notification");

        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);

        //set-up the action for authorizing the action
        Intent intent = new Intent(getApplicationContext(), MainActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);


        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(this)
                        .setSmallIcon(R.drawable.ic_launcher)
                        .setContentTitle(this.getString(R.string.app_name))
                        .setAutoCancel(Boolean.TRUE)
                        .setContentText("You are going to " + action);


        builder.setContentIntent(pendingIntent);

        notificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
    }
}



MyBroadcastIntentServiceTest
Our IntentService test will have to test cases: one without a bundle (which should display no notification) and one with a correctly provided bundle (which does display 1 notification).

In the latter case we will also verify that the text (displayed in the notification) contains "You are going to eat an apple", which contains the actual action that was sent using the intent in the test case.

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowNotification;
import org.robolectric.shadows.ShadowNotificationManager;

import be.acuzio.mrta.service.MyBroadcastIntentService;


@RunWith(RobolectricTestRunner.class)
public class MyBroadcastIntentServiceTest {
    static {
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    @Test
    public void testNoBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);

        Assert.assertEquals("Expected no notifications", 0, Robolectric.shadowOf(notificationManager).size());
    }

    @Test
    public void testWithBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        Bundle bundle = new Bundle();
        bundle.putString("ACTION", "eat an apple");
        serviceIntent.putExtras(bundle);

        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);


        ShadowNotificationManager manager = Robolectric.shadowOf(notificationManager);
        Assert.assertEquals("Expected one notification", 1, manager.size());

        Notification notification = manager.getNotification(MyBroadcastIntentService.NOTIFICATION_TAG, MyBroadcastIntentService.NOTIFICATION_ID);
        Assert.assertNotNull("Expected notification object", notification);

        ShadowNotification shadowNotification = Robolectric.shadowOf(notification);
        Assert.assertNotNull("Expected shadow notification object", shadowNotification);

        Assert.assertEquals("You are going to eat an apple", shadowNotification.getLatestEventInfo().getContentText());
    }

    class MyBroadcastIntentServiceMock extends MyBroadcastIntentService {
        @Override
        public void onHandleIntent(Intent intent) {
            super.onHandleIntent(intent);
        }
    }
}


The test class contains the MyBroadcastIntentServiceMock (might deserve a better name) which extends the MyBroadcastIntentService. By overriding and exposing the onHandleIntent(Intent intent) method we are able to actually test the code.

dinsdag 11 februari 2014

Robolectric

At a certain point in time in your life you realise that you're done fooling around (in the technical sense), and that quality - next to Love - conquers all. In the past weeks, I had an epiphany and realised I had to focus more on test driven and continuous development in order to improve the quality of (my) Android coding.

Thus started my personal quest "How is Test Driven Development (TDD) accomplished on Android". All Java-lovers including myself will somehow utter the words Unit Testing and Continuous Integration. True, but how do you manage this on a platform which is mobile and where device emulators, well, frankly s*ck.




I finally ended up with Robolectric.org, a framework that allows you to run your code against your JVM. Yes, your JVM.  Anxious as a 6-year-old that just got some presents from Santa, I started coding.

... and got stuck. Where the hell is the documentation? Oh right, Stackoverflow.

Frustrated about the copy-pasting from Stackoverflow, I wanted to share some of the examples I found/used that hopefully will ease-up your life, and help you develop up-spec quality Android applications.

Imaging that you developed an activity that fetches information from the Extras bundle passed in your intent:
public class MyActivity extends Activity {

    private final static String TAG = MyActivity.class.getSimpleName();


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.my_layout);



        Bundle bundle = getIntent().getExtras();



        String resource_url  = (bundle == null ? "" : bundle.getString("URL", ""));

        String resource_name = (bundle == null ? "" : bundle.getString("RESOURCE", ""));




        ((TextView) this.findViewById(R.id.txt1)).setText(resource_name);

        ((TextView) this.findViewById(R.id.txt2)).setText(resource_url);


    }


}


You would like to know that when you pass the information via the intent, that it will place the corresponding strings in the foreseen TextViews.


package ...
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.util.ActivityController;

import ...



@RunWith(RobolectricGradleTestRunner.class)
public class MyActivityTest {
    @Before
    public void setUp() {
       //whatever you need to have done before a test is run
    }    

    @Test
    public void testIfTextFieldsAreFilled() {
        Bundle bundle = new Bundle();
        bundle.putString("URL", "http://www.google.be");
        bundle.putString("RESOURCE", "Google");

        Intent intent = new Intent(Robolectric.getShadowApplication().getApplicationContext(), MyActivity.class);
        intent.putExtras(bundle);

        Activity activity = Robolectric.buildActivity(MyActivity.class).withIntent(intent).create().get();
        ShadowActivity testActivity = Robolectric.shadowOf(activity);

        Assert.assertEquals("Google", ((TextView) testActivity.findViewById(R.id.txt1)).getText().toString());         
        Assert.assertEquals("http://www.google.be", ((TextView) testActivity.findViewById(R.id.txt2)).getText().toString());
    }

}


There is actually quite an important difference. You would expect that you would test against the "built" Activity, but you need to invoke the ShadowActivity via Robolectric.shadowOf(...);. If you don't, your test code will not pass the intent correctly and your test results will fail.

        
        Activity activity = Robolectric.buildActivity(MyActivity.class).withIntent(intent).create().get();
        ShadowActivity testActivity = Robolectric.shadowOf(activity);

Happy testing! Happy Developer! Happy Customer!

maandag 3 februari 2014

Test Driven Development with Robolectric and Android Studio

Sometimes I believe that when R.R. Martin was writing "A Song of Ice and Fire", he was actually referring to my relation with Gradle. Until the beginning of this year, I was a profound believer of the modular Eclipse platform, and did develop anything with it: from PHP (PDT) to Java/Android (ADT), even Node.JS passed its 'revue'. As a new challenge, I downloaded the (IntelliJ-based) Android Studio IDE for my new project. Google decided to use Gradle as it foundation build-automation-platform, and even went thus far that it integrated it seamlessly in the IDE experience.

Sometimes you need a new challenge and went full speed ahead. Android Studio is still in development (the current Canary build marks 0.4.4) and can thus give you some head-aches ("why can't I remove a module with a push of a button", "why do I need to write code when I just want to add a jar-file to my build path", "what the hell is Gradle doing with my classpath?", etc). I must say that between the stable 0.3.5 release in the beginning of the year, and the 0.4.4 Canary build from February, that a lot of progress has been made.

Let's get back to the topic: you have started a new project and are going to add Robolectric to your TDD Project. How do you handle it?

When you just started a clean project, your Gradle file looks more or less like this



As we are promoting Test Driven Development (TDD), let's first start on setting-up the Robolectric installation for Android Studio.

First, create a new location for the tests. I like to keep my tests on the same level as my sources, so I created a folder /src/test in my "app" module.
  • Right click the "src" folder
  • Select "New" > "Directory"
  • Enter "test/java" as name and press "OK"
If all went according to plan, two new folders have been created



As you might notice, the first (main/java) folder is marked in blue, which means that it is actually a source folder. In Eclipse you would add the /test/main folder to your source configuration, but this is Android Studio... we need Gradle to do this for us.


We are all code-monkeys, so let's start with the build.gradle file. Open the file in the "app" module ("/<project>/<module>/build.gradle" and not "/<project>/build.gradle" which is on your root) and match it to the one below:


May 26th 2014: with recent developments in Android Studio and Robolectric, please check out this update on the build script and how to test. Most of this blogpost remains valid though.


buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
    }
}

allprojects {
    repositories {
        mavenCentral()
    }
}

apply plugin: 'android'

android {
    compileSdkVersion 19
    buildToolsVersion "19.0.0"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    sourceSets {
        instrumentTest.setRoot('src/test')
    }
}

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

Now update your project to match the Gradle build file, via Tools > Android > Sync Project with Gradle Files.

When you added the instrumentTest.setRoot('src/test'), you actually told Gradle that your tests are located in the src/test/java folder, which is now marked in green.



Before we configure Robolectric further, let's first create a new package and a dummy test case.


Next on the list is telling Gradle that we are using Robolectric and jUnit for running our test cases. The libraries are available from Maven repositories and we need to modify our classpath. This modification needs to be done in the build.gradle file where we added the test sources.

buildscript {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
        classpath 'com.novoda.gradle:robolectric-plugin:0.0.1-SNAPSHOT'
    }
}

allprojects {
    repositories {
        mavenCentral()
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
    }
}

apply plugin: 'android'
apply plugin: 'robolectric'

android {
    compileSdkVersion 19
    buildToolsVersion "19.0.0"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    sourceSets {
        instrumentTest.setRoot('src/test')
    }
}

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

    androidTestCompile 'org.robolectric:robolectric:2.+'
    androidTestCompile 'junit:junit:4.+'
}

Synch your project again via Tools > Android > Synch Project with Gradle Files.

Now lets create our first Test, which is programmed to fail.


import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

/**
 * Created by vandekr on 11/02/14.
 */
@RunWith(RobolectricTestRunner.class)
public class DummyTest {
    @Before
    public void setup() {
        //do whatever is necessary before every test
    }

    @Test
    public void testShouldFail() {
        Assert.assertTrue(Boolean.FALSE);
    }
}

Now Hold On! How do you run these tests? Well, Gradle will be your Holy Grail here: we told it to install the robolectric plugin from the snapshot maven repository. Android Studio is truly an Integrated Development Environment, as it gives you a terminal (yes, a terminal, like old-school-vi-programming).

Open the terminal by clicking on the "Terminal" button in the bottom left corner of the IDE



You can start the gradle build with the gradlew command. When you first open the terminal you will need to navigate one level lower where the gradlew command is located, and start it with the command ./gradlew robolectric

MBA:app $ cd ..
MBA:MyRobolectricTestApp $ ./gradlew robolectric

This will build your application and start the robolectric tests, just like that!

PS: if you get a "Permission denied", it means that the ./gradlew file is not executable. Just chmod it!

MBA:MyRobolectricTestApp $ ./gradlew robolectric
bash: ./gradlew: Permission denied
MBA:MyRobolectricTestApp $ chmod a+x ./gradlew

The result should look something like this:

:app:preDexRelease             
:app:dexRelease             
:app:processReleaseJavaRes UP-TO-DATE  
:app:packageRelease             
:app:assembleRelease             
:app:assemble             
:app:compileRobolectricJava                                        
Note: ./AndroidStudioProjects/MyRobolectricTestApp/app/src/test/java/be/acuzio/mrta/test/DummyTest.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
:app:processRobolectricResources UP-TO-DATE  
:app:robolectricClasses             
:app:robolectric                                                   
                             
be.acuzio.mrta.test.DummyTest > testShouldFail FAILED
    junit.framework.AssertionFailedError at DummyTest.java:22
                                                          
1 test completed, 1 failed                                
:app:robolectric FAILED                                   
          
FAILURE: Build failed with an exception.
          
* What went wrong:
Execution failed for task ':app:robolectric'.
> There were failing tests. See the report at: file:///./AndroidStudioProjects/MyRobolectricTestApp/app/build/reports/tests/index.html
          
* Try:    
Run with --stacktrace option to get the stack trace. Run with --inf or                                                                 --debug                                                              option to get more log output.
          
BUILD FAILED
          
Total time: 18.536 secs
MBA:MyRobolectricTestApp $ 

Wonderful! It actually did what we expected. Now let's update the test code and change the assertion to Boolean.TRUE. Re-run the tests using the ./gradlew robolectric command in your terminal.

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

/**
 * Created by vandekr on 11/02/14.
 */
@RunWith(RobolectricTestRunner.class)
public class DummyTest {
    @Before
    public void setup() {
        //do whatever is necessary before every test
    }

    @Test
    public void testShouldFail() {
        Assert.assertTrue(Boolean.TRUE);
    }
}

Your terminal should now display the message "BUILD SUCCESSFUL".  Eureka!

What we just proved was that we can run tests from Gradle, but we are not there yet: we want to test our Android code, which uses Activities, Services and all of these nifties things.

Create  a new Test Class called MainActivityTest and match it to the code below.

import android.app.Activity;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;

import be.acuzio.mrta.MainActivity;

/**
 * Created by vandekr on 11/02/14.
 */
@RunWith(RobolectricTestRunner.class)
public class MainActivityTest {
    @Before
    public void setup() {
        //do whatever is necessary before every test
    }

    @Test
    public void testActivityFound() {
        Activity activity = Robolectric.buildActivity(MainActivity.class).create().get();

        Assert.assertNotNull(activity);
    }
}


We actually tell Robolectric to create a new Activity (MainActivity) and to return it. We then assert that it is not null.

Now re-run the ./gradlew robolectric command, you should get a "BUILD SUCCESSFUL" for both tests.  Now you are ready to start developing and create your unit tests!

Gradle and Robolectric render an HTML file containing your test results, which is quite nice in a Continuous Integration environment. It's located in your Android Studio project folder, in the project build/report folder. When you open it, it will provide a nice overview of the results with passes and fails.



I'll post some more testing examples, in next weeks to help you improve the quality of your Android project. Please feel free to comment and propose improvements!

May 26th 2014: with recent developments in Android Studio and Robolectric, please check out this update on the build script and how to test. Most of this blogpost remains valid though.

July 24th 2014: if you run into problems, please check-out the source code, which is freely (as-is) available on Github: https://github.com/kvandermast/my-robolectric-app/