Freitag, 17. Januar 2014

Getting started with Robolectric and Gradle in Android Studio

Unit tests are an invaluable tool to ensure software behaves the way it is supposed to which you want to check as often as possible while developing. However, running tests on an Android emulator is slow. Robolectric is a unit test framework that enables you to run your Android unit tests as plain vanilla JUnit tests inside the JVM on your computer, which is much faster and less cumbersome than to run them inside the emulator.

New Android projects are built with Gradle

There is a plugin for Gradle called gradle-android-test-plugin that will run unit tests relying on Robolectric and compile the results in a test report. The following will describe how to change your build.gradle to set this up. First, pitfalls along the way will be shown, followed by a working build.gradle configuration. Finally, a few tweaks have to be made to integrate this properly with Android Studio.

Pitfalls and how to work around them

These are problems I encountered when trying to set up the gradle-android-test-plugin and how to circumvent them.

Gradle cannot determine dependencies

* What went wrong:
Could not determine the dependencies of task ':MyApp:testDefaultFlavorDebug'.
> A base directory must be specified in the task or via a method argument
Solution: Use 0.9.1-SNAPSHOT version of the gradle-android-test-plugin., it fixes this problem.

Robolectric: Resource path restrictions

java.lang.IllegalArgumentException: Resource path must end in "/res"
    at org.robolectric.res.PackageResourceLoader.<init>(PackageResourceLoader.java:20)
    at org.robolectric.res.PackageResourceLoader.<init>(PackageResourceLoader.java:11)
Solution: Use 2.3-SNAPSHOT version of Robolectric, it fixes this restriction on the path name.

Robolectric: No KitKat

java.lang.UnsupportedOperationException: Robolectric does not support API level 19, sorry!
Solution: As defined per SdkConfig.java, Roboelectric only supports SDKs 16 through 18. Change targetSdkVersion to 16, 17 or 18.

Summary of necessary changes to build.gradle

Your build.gradle has to be changed in quite a few places to set up gradle-android-test-plugin. Here's the condensed summary of theses changes:
buildscript {
    repositories {
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
    }
    dependencies {
        classpath 'com.squareup.gradle:gradle-android-test-plugin:0.9.1-SNAPSHOT'
    }
}
apply plugin: 'android-test'
repositories {
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
dependencies {
    testCompile 'junit:junit:4.10'
    testCompile 'org.robolectric:robolectric:2.3-SNAPSHOT'
    testCompile 'com.squareup:fest-android:1.0.+'
}

Write a first unit test

Now you can create a unit test. Create a file in src/test/com.myapp.MyTest.java:
package com.myapp;

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

import static org.junit.Assert.assertTrue;

@RunWith(RobolectricTestRunner.class)
public class MyTest {
    @Test
    public void testNumberOne() {
        assertTrue(false);
    }
}
Note that with Robolectric 2.3-SNAPSHOT you do not need RobolectricGradleTestRunner anymore, since it is integrated. When you now run ./gradlew test in your project, it should run that very unit test, which will -- by design -- fail:
com.myapp.MyTest > testNumberOne FAILED
    java.lang.AssertionError at MyTest.java:13

Integrating with Android Studio

So far, we have a working set up of Gradle, Robolectric and the gradle-android-test-plugin -- but only for the console. When we try to "Sync Project with Gradle Files" in Android Studio, the IDE will not pick up our newly created src/test folder and it will not provide content assist when writing code.
This is because Android Studio expects test code to be in src/instrumentationTest, but luckily we can change that in build.gradle:
android {
    sourceSets.instrumentTest.setRoot('src/test')
}
Android Studio also cannot compile the test code because it misses dependencies. This is because it expects these dependencies to be defined in the configuration named instrumentTestCompile, whereas currently, we are using the testCompile configuration only. We have to add the dependencies defined earlier to the configuration Android Studio uses. This can even be achieved without repeating them, but by creating a copy, like so:
dependencies {
    instrumentTestCompile configurations.testCompile.dependencies
}

Summary

This article described how to overcome the problems when adding the gradle-android-test-plugin to a project. It provided a configuration for Gradle to run a sample unit test leveraging the Roboelectric library. In the last step, it showed how to alter the configuration so that Android Studio would play along.

Donnerstag, 9. Januar 2014

Using Play JSON Combinators to fill a Map

Reads[T] to case object

Play Framework provides a neat library to turn JSON into Scala Objects. Typically, you would use play.api.libs.json.Reads[T] like this:
case class Vote (
    yes: Int,
    no: Int,
    maybe: Int
)

implicit val voteReads = (
    (__ \ "yes").read[Int] and
    (__ \ "no").read[Int] and
    (__ \ "maybe").read[Int]
)(Vote.apply _)
This will turn a flat JSON dictionary representing the results of a vote into a case object.

Reads[T] to Map

However, you might not always want to create case objects from your JSON input. You might want to collect the results from the Reads[T] in a Map, mapping from the choices in a vote to the number of votes. It is not immediately obvious how to achieve this, but keep in mind that the above statement can also be represented as:
implicit val voteReads = (
    (__ \ "yes").read[Int] and
    (__ \ "no").read[Int] and
    (__ \ "maybe").read[Int]
)((yes: Int, no: Int, maybe: Int) => new Vote(yes, no, maybe)) 
With this in mind, you can easily replace the creation of the case object with the creation of a Map using Reads Combinators like this:
import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val voteReads = (
    (__ \ "yes").read[Int] and
    (__ \ "no").read[Int] and
    (__ \ "maybe").read[Int]
)((yes: Int, no: Int, maybe: Int) => Map("yes" -> yes, "no" -> no, "maybe" -> maybe))

val js = Json.obj( "yes" -> 7, "no" -> 11, "maybe" -> 13)
js.validate
res0: play.api.libs.json.JsResult[scala.collection.immutable.Map[String,Int]] =
 JsSuccess(Map(yes -> 7, no -> 11, maybe -> 13),)