Thursday, January 6, 2011

Easy unit testing for Android

Testing and Mocking

Everyone who is, like me, 'spoiled'  by mocking frameworks like Mockito, EasyMock, etc know the pain of trying to write compact and readable unit tests for Android. Of course there is JUnit built-in support in Android that offers the whole battery of base classes in order to perform integration testing. And, surely, there is a number of base mock classes that are supposed to allow proper unit testing. However when it comes to writing the tests those mock classes are not really 'nice' mocks. They are not really that 'nice' because they do stuff like:

/**
 * A mock {@link android.content.Context} class.  All methods are non-functional and throw 
 * {@link java.lang.UnsupportedOperationException}.  You can use this to inject other dependencies,
 * mocks, or monitors into the classes you are testing.
 */
public class MockContext extends Context {

    @Override
    public AssetManager getAssets() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Resources getResources() {
        throw new UnsupportedOperationException();
    }
...

}

As you can see it throws exceptions on every invocation, which means that if you want to use it you have to extend it. Problem is that it almost never limited by overriding just one method. And because your test cases are probably different the re-use of the extended MockContext is pretty limited and, again, if you try to create a re-usable MockContext the amount of effort will be somewhat close to re-writing the platform classes! So to go on with it you might start creating a number of subclasses of MockContext's to serve your particular needs. By the way, this is exactly what all the built-in subclasses of the MockContext do - even though they are a part of the public API if you have a closer look at them you might notice those are written for some particular cases to test the Android platform itself! But even those are not generic enough as you can see in this example:

RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
                new MockContext2(), // The context that most methods are delegated to
                getContext(), // The context that file methods are delegated to
                filenamePrefix);

As you can see all sorts of compositions, extensions happen a lot.

Mockito to the rescue?

"Use a mocking framework" - many will probably think. Well, it's not as simple. The problem is that Dalvik VM is not really a Java VM. And most known pure-java mocking frameworks wont work out of the box, e.g. http://code.google.com/p/mockito/issues/detail?id=57
Some mocking frameworks require interfaces, which Android isn't really full of (for good reasons given the circumstances). Some mocking frameworks can do more than others and some have more or less ease of use.
Besides, if you base your classes on Android base TestCases it will run only on the emulator which isn't fast enough for unit testing.

So use the standard Java VM?

Right, the mocking frameworks will shine again! Not as simple again, you would need to mock every single call to classes contained in the Android platform (many of which are static!) which is probably even worse than just extending Android built-in mock classes.
Well, some (even people at Google) try to do it, but when I tried to repeat it with the real code I just gave up on the amount of work needed to be done just to perform simple testing.

Stumbled upon


And then I stumbled upon Robolectric. Robolectric is capable of either creating 'nice' stubs that don't throw RuntimeException("Stub!") (which is what happens if you use classes from distributable android.jar) or delegating calls to the classes in the Android platform. Basically, you can run unit tests outside the emulator in super-fast Java VM with all the mocking frameworks you like!

This allowed me to use my favorite Mockito without the need of having a separate test project, I simply employed maven!

There are plenty of articles on the Internet on how to mavenize android projects. My setup isn't much different from those so I will just focus on further customization. Quickly, I enabled dependencies to run Robolectric and Mockito in the test phase:


        
            com.pivotallabs
            robolectric
            0.9.5
            test
        
        
            junit
            junit
            4.8.2
            test
        
        
            org.mockito
            mockito-all
            1.8.5
            test
        
        
            com.google.android
            android-test
            test
            2.2.1
        
        
            com.google.android
            android
            2.2.1
            provided
        
    

If you notice I use a version of JUnit somewhat newer than the one bundled with Android SDK.
When we want to write a test case we annotate it with the Robolectric runner which does all the magic of running the unit test code on regular JVM:

@RunWith(RobolectricTestRunner.class)
public class HudsonViewProcessorUnitTest {

Creating Mockito mocks then is nothing but the usual:

@Before
    public void setUp() {
        contextMock = mock(Context.class);
        parserMock = mock(ViewJsonParser.class);
        hudsonStatusDaoMock = mock(HudsonStatusDAO.class);
        hudsonJobDaoMock = mock(HudsonJobDAO.class);
    }

Note, that I create a mock of the regular Context class in Android. And since Mockito creates 'nice' mocks by default in my particular case I didn't have to stub much of it!

Creating a test case is also very straight forward:

@Test
    public void testSimple() throws BuildParsingFailedException {
        HudsonViewProcessor hudsonViewProcessor = new HudsonViewProcessor(contextMock, defaultStatusBean, true);
        injectMocks(hudsonViewProcessor);
        List<HudsonJobBean> hudsonJobs = createHudsonJobs(WIDGET_ID, "blue", "yellow", "red");

        when(parserMock.parseHudsonJobs()).thenReturn(hudsonJobs);

        ...
    }

You might notice I don't use dependency injection which is, again, not supported by Android by-default. So I pass dependencies either via constructor or via setter methods (I have used both ways in this example) which allows me to reduce discomfort of not having dependency injection and still having the code testable.

My next stop would be to have a look at RoboGuice and see if dependency injection is usable and fast enough on Android after JIT was introduced.

The combination of the two frameworks Rolectric and Mockito made unit testing on Android fun again!
Published with Blogger-droid v1.6.5

5 comments:

Christopher Perry said...

Dude, you are awesome. Thanks for the post. I'm going to try to get my tests automated in Jenkins. Any advice on that?

sdudzin said...

Testing in Jenkins should just work - nothing special there. I would also advise on looking into dependency injection framwork like roboguice - it will make code so much cleaner and easier to test!

Christopher Perry said...

Did you every end up getting roboguice to work with this setup?

sdudzin said...

I actually did and it works quite well!

Unknown said...


I got this exception when i try to spy an injected object with mockito.

Can anybody help me??


java.lang.RuntimeException: java.lang.RuntimeException: Stub!
at com.google.dexmaker.DexMaker.generateAndLoad(DexMaker.java:388)
at com.google.dexmaker.stock.ProxyBuilder.buildProxyClass(ProxyBuilder.java:252)
at com.google.dexmaker.mockito.DexmakerMockMaker.createMock(DexmakerMockMaker.java:56)
at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:26)
at org.mockito.internal.MockitoCore.mock(MockitoCore.java:51)
at org.mockito.Mockito.mock(Mockito.java:1243)
at org.mockito.Mockito.mock(Mockito.java:1120)
at com.devsu.supermaxi.services.test.ClientValidationServiceTest.setUp(ClientValidationServiceTest.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:234)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:175)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: java.lang.RuntimeException: Stub!
at dalvik.system.BaseDexClassLoader.(BaseDexClassLoader.java:5)
at dalvik.system.DexClassLoader.(DexClassLoader.java:5)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
at com.google.dexmaker.DexMaker.generateAndLoad(DexMaker.java:382)
... 32 more