This proved a bit hard to do. I won't go into the details of integrating Scala and Android, since Christian Neukirchen did a good job of that with his article Programming for Android with Scala.
After following the advice in that class, you basically have to double it to compile, dex, and trim the test tree as well.
The big issue I had was trying to use ScalaTest with Android testing. As far as I know, no one has tried to do this yet.
First, Android testing is very particular. You can't just run any old class on the phone, instead there is a particular runner called the
InstrumentationTestRunner
to run tests. So, you need either Android-specific test classes, or JUnit classes. I therefore had to write a converter class:
class ScalaUnitTestRunner extends TestCase {
def testExerciseList() = {
new ExerciseListDaoSpec().execute()
}
}
Simple and needs improvement, but it's the most basic thing that can work. The
execute
call runs the scalatests in that test class.The problem was that when running, I would get this error on running:
[exec] android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests:
[exec] Error in testSuiteConstructionFailed:
[exec] java.lang.RuntimeException: Exception during suite construction
[exec] at android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests.testSuiteConstructionFailed(TestSuiteBuilder.java:239)
[exec] at java.lang.reflect.Method.invokeNative(Native Method)
[exec] at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:164)
[exec] at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:151)
[exec] at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:418)
[exec] at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1520)
[exec] Caused by: java.lang.TypeNotPresentException: Type java.rmi.RemoteException not present
[exec] at java.lang.Class.getDeclaredMethods(Native Method)
[exec] at java.lang.ClassCache.getDeclaredPublicMethods(ClassCache.java
[exec] at java.lang.ClassCache.getDeclaredMethods(ClassCache.java:179)
[exec] at java.lang.ClassCache.findAllMethods(ClassCache.java:249)
[exec] at java.lang.ClassCache.getFullListOfMethods(ClassCache.java:223)
[exec] at java.lang.ClassCache.getAllPublicMethods(ClassCache.java:204)
[exec] at java.lang.Class.getMethods(Class.java:1038)
[exec] at android.test.suitebuilder.TestGrouping.getTestMethods(TestGrouping.java:79)
...
The problem was that for some reason every Scala class contains a special method called
$tag
. If you used javap
to investigate, you'd find the class looks like:~/src/AshExercises $ javap -classpath testbin/classes com.ash.exercisetests.db.ScalaUnitTestRunner
Compiled from "ScalaUnitTestRunner.scala"
public class com.ash.exercisetests.db.ScalaUnitTestRunner extends junit.framework.TestCase implements scala.ScalaObject{
public com.ash.exercisetests.db.ScalaUnitTestRunner();
public void testExerciseList();
public int $tag() throws java.rmi.RemoteException;
}
Because Android's tester is using reflection here to find the test methods, it encounters this method with a class that Android doesn't know about. I tried to define a fake java.rmi.RemoteException, but Android is smart and doesn't let you define core java classes.
So, I had to change my tester to this:
class ScalaUnitTestRunner extends TestCase {
def testExerciseList() = {
new ExerciseListDaoSpec().execute()
}
override def $tag() : Int = {
try {
return super.$tag();
} catch {
case e: Exception => throw new RuntimeException(e);
}
}
}
That fixed that issue.
The next issue it had was that Android was giving me
java.lang.VerifyError
on running. This turned out to be my fault, but what I had to figure out was that java.lang.VerifyError
really meant a ClassCastException. I found this out by looking in the logcat.E/dalvikvm( 726): Could not find class 'com.ash.exercise.db.ExerciseDatabase', referenced from method com.ash.exercisetests.db.ExerciseListDaoSpec.<init>
W/dalvikvm( 726): VFY: unable to resolve new-instance 13 (Lcom/ash/exercise/db/ExerciseDatabase;) in Lcom/ash/exercisetests/db/ExerciseListDaoSpec;
W/dalvikvm( 726): VFY: rejecting opcode 0x22 at 0x0019
W/dalvikvm( 726): VFY: rejected Lcom/ash/exercisetests/db/ExerciseListDaoSpec;.<init> ()V
W/dalvikvm( 726): Verifier rejected class Lcom/ash/exercisetests/db/ExerciseListDaoSpec;
I/TestRunner( 726): failed: testExerciseList(com.ash.exercisetests.db.ScalaUnit
It turned out I was compiling against the non-test src, but wasn't including the non-test src in my proguard step, so that the classes never wound up in my apk.
I hope these tidbits help someone who is exploring Android, Scala, and unit tests. Good luck, and happy coding!