Test-Driven Android: When and How I Use Robolectric

I’ll call this post “When and How I Use Robolectric” instead of “When and How to Use Robolectric” because a) I don’t like telling people when and how to do things, and b) I use Robolectric less liberally than most developers who use it.

Robolectric is a fantastic tool for ensuring that stuff in your Android app works properly. The problem, as you may have heard me say before, is that it runs slowly. In particular, FragmentTestUtil’s operations are especially time-expensive. Compounding the issue, Robolectric 3.0 currently has a memory leak that causes the heap to get bigger than it should. You won’t notice it on a smaller app, but when the app grows enough for the test suite to hit 600 or 700 Robolectric-assisted tests, the suite will hang or stop running because it runs out of memory before it completes.

So you only have 600-ish Robolectric bucks in the bank, and you spend one every time you write a test with Robolectric. My team at work, and I at home, have experimented with ways to save our Robolectric bucks. The first (main) trick has been to separate as much logic from the Android framework as possible and test that logic with JUnit. We save our precious Robolectric bucks this way, spending them on just a test or two for the places where the logic gets wired into the Android app.

It helps to understand exactly what Robolectric is and what it does.

Robolectric allows you to get a shadow of most Android classes that you need to test.

These include activities, intents, and the like. Most Robolectic-using test suites will get shadows of these things to make sure that Android did what the developer wanted it to do: start an activity, start a service, send a broadcast. I don’t do this nearly as much as I used to. The thing about it is that, if I make sure that an activity has been started, I’m not just testing my code: I am also testing the Android framework. If the activity does not start because of something in the framework, I have no control over that and I also have no idea why my test is failing. So instead, I prefer to write automated tests that check whether my code asked Android to start an activity. I don’t test whether the activity actually got started. I can do this with spies of my test subjects, or I can inject contexts into my dependencies and test that methods like startService() got called on them. These tests use plain old test doubles—not Robolectric.

That said, Robolectric still saves the day for testing notoriously hard-to-test code. In particular, I’m thinking of static methods.

Android has a lot of static methods. LocalBroadcastManagers use them to get the manager instance. Fragments that need to be instantiated with data commonly use a static newInstance() method to do so. I have yet to do a single thing with bitmaps that doesn’t require a static method.

When you don’t have an instance, you can’t get a test double of the instance on which to check that the method has been called. If we can’t get a snapshot of the animal we’re looking for, we have to look for its feetprints to know if it has been here. The programming equivalent of looking for feetprints is to look for the result of the method having been called, since we can’t verify its call in a more direct fashion.

This is when I pull out Robolectric. Because Robolectric isn’t just for activities and fragments: it allows you to get a shadow of most Android classes that you need to test. We know that it is an Android class by looking at its package in the import statement. For example:

import android.graphics.Bitmap;
import android.support.v7.app.AppCompatActivity;
import android.support.v4.content.LocalBroadcastManager;

These things all come from Android.

So when we face a static method from one of these things, we can often test for the presence of the effects of the method we want called, like so:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 18)
public class ImageDecoderTest {
    @Test
    public void doToMyBitmap() throws UnsupportedEncodingException {
        BitmapDoSomethingClass subject = new BitmapDoSomethingClass();
        Bitmap output = subject.doToMyBitmap(inputBitmap);
        ShadowBitmap shadowBitmap = Shadows.shadowOf(output);
        assertThat(shadowBitmap.undoStuffDoneInDoToMyBitmap().isEqualTo(inputBitmap);
    }
}

We look for the feetprints of doToMyBitmap() by undoing the effects of that method on a shadow of that method’s output, and making sure that we get out what we put in. Mind you, this approach is not without flaws: by virtue of the fact that the test undoes exactly what the test method did, this test is tightly coupled to the class’s implementation, rather than its behavior. That having been said, this does not bother me too much for two reasons:

  1. We are testing the manipulation of a bitmap, all of which happens through the Android interface. There are a very limited number of ways we could achieve this same thing with a different implementation at this low level. The need to change this implementation would come, if it came, from a fundamental change in the Android interface. It’s possible, but it’s not likely enough for me to spend inordinate amounts of time looking for another way here.
  2. I look at this test, and I think about my alternatives. The list of alternatives is slim. That list includes not testing this functionality, and I’m not okay with that. Above all else, I want to know that, if the behavior of my code gets mutated or removed, a test fails. I want this to guard against regression. I want this to guard against mistakes. But most of all, I want this so that any other developer who walks into the code base can find out exactly what any part of it does by commenting out the line s/he doesn’t understand and seeing a test fail. Those tests document my code. The above test satisfices for providing the benefits of tests that I want the most.

The Robolectric tests may run a little slower, and they may have their concerns. But when faced with a static method within the Android framework, they’re providing a way forward —at least until I alight upon a better solution.


			

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s