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/



Geen opmerkingen:

Een reactie posten