Android

Unit testing Android apps with Robolectric: advanced use cases

Learn how the Robolectric API is useful for various Android unit tests, such as when you're working with delayed and background tasks.

In part one of my series about Roboelectric, I explored how the framework makes unit testing Android apps possible. The examples in this installment provide advanced use cases utilizing the Robolectric API. 

Testing delayed and background tasks

It is a common practice to initiate delayed tasks that execute after a specified period of time; one example is re-enabling a button after a specific timeframe. Background tasks are also a necessity to avoid "Application Not Responding" (ANR) errors for long-running tasks.

Delayed and background tasks pose a challenge for testing, as they are executed asynchronously while tests execute synchronously. Robolectric has an API to control when delayed and background tasks are executed during a test.

For example, let's say we are testing a method that sets a boolean value after one second has expired.

public class DelayedRunner {
  private boolean executed = false;

  public void execute() {
    new Handler().postDelayed(new Runnable() {
      @Override
      public void run() {
        executed = true;
      }
    }, 1000);
  }

  public boolean isExecuted() {
    return executed;
  }
}

The test case below fails when asserting that the executed variable was set.

@RunWith(RobolectricTestRunner.class)
public class DelayedRunnerTest {
  @Test
  public void testExecute() {
    DelayedRunner delayedRunner = new DelayedRunner();
    delayedRunner.execute();
    
    Assert.assertTrue(delayedRunner.isExecuted());
  }
}

Adding a call to Robolectric.runUiThreadTasksIncludingDelayedTasks() just prior to the assertion ensures that the delayed method executes, and we can assert the expected result.

@RunWith(RobolectricTestRunner.class)
public class DelayedRunnerTest {
  @Test
  public void testExecute() {
    DelayedRunner delayedRunner = new DelayedRunner();
    delayedRunner.execute();
    
    Robolectric.runUiThreadTasksIncludingDelayedTasks();

    Assert.assertTrue(delayedRunner.isExecuted());
  }
}

Robolectric also provides the Robolectric.runBackgroundTasks() method, which ensures any pending background tasks will immediately execute during a test. Assertions can then apply to the results of those tasks.

Using built-in shadow objects

The Robolectric testing framework provides "shadow objects" that override certain aspects of the Android SDK. These objects allow the code being tested to execute outside of the Android environment. At times, it's useful to manipulate these shadow objects to achieve some expected result.

For example, the application could save files to the SD card, and we need to test the behavior of our file utility.

public class FileUtil {
  public static String saveToFile(String filename, String contents) {
    String storageState = Environment.getExternalStorageState();
    
    if(!storageState.equals(Environment.MEDIA_MOUNTED)) {
      throw new IllegalStateException("Media must be mounted");
    }

    File directory = Environment.getExternalStorageDirectory();
    File file = new File(directory, filename);
    FileWriter fileWriter;

    try {
      fileWriter = new FileWriter(file, false);
      fileWriter.write(contents);
      fileWriter.close();

      return file.getAbsolutePath();
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
}

The test can initialize the ShadowEnvironment object provided by Robolectric to verify the method behavior in each scenario.

@RunWith(RobolectricTestRunner.class)
public class FileUtilTest {
  @Test
  public void testSaveToFile() {
    String filename = "test.txt";
    String expectedContents = "test contents";

    ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
    String absolutePath = FileUtil.saveToFile(filename, expectedContents);

    File expectedFile = new File(absolutePath);
    Assert.assertTrue(expectedFile.exists());
    expectedFile.delete();
  }

  @Test(expected = IllegalStateException.class)
  public void testSaveToFile_mediaUnmounted() {
    String filename = "test.txt";
    String expectedContents = "test contents";

    ShadowEnvironment.setExternalStorageState(Environment.MEDIA_UNMOUNTED);
    FileUtil.saveToFile(filename, expectedContents);
  }
}

Many of the shadows provided by Robolectric provide additional behavior not available within the Android SDK. This behavior simplifies testing for scenarios like the one above. You can review the behavior provided by all shadow classes in the JavaDocs.

Not all shadows will be accessed statically as in the example above. The shadow of an Android object instance can be retrieved using the Robolectric.shadowOf method. Using an example from the Robolectric documentation, an ImageView could have a drawable resource associated with it.

<ImageView
    android:id="@+id/header"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:src="@drawable/header_image">

There is no way through the ImageView class to access the resource id and verify its value. The following test demonstrates how this could be accomplished with Robolectric.

@Test
public void testHeaderImage() throws Exception {
  ImageView header = (ImageView) activity.findViewById(R.id.header);
  ShadowImageView shadowHeader = Robolectric.shadowOf(header);
  assertThat(shadowHeader.resourceId, equalTo(R.drawable.header_image));
}

Preparing the test environment

There may be certain aspects of a test environment that need to be initialized before and reset after every test. Robolectric has extension points that allow for this.

The following test runner implementation provides some setup and teardown appropriate for our previous tests.

public class ExtendedRobolectricTestRunner extends RobolectricTestRunner {
  public ExtendedRobolectricTestRunner(Class<?> testClass)
      throws InitializationError {
    super(testClass);
  }
	
  @Override
  public void beforeTest(Method method) {
    super.beforeTest(method);
    // setup the environment expected by all tests
    ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
  }
	
  @Override
  public void afterTest(Method method) {
    super.afterTest(method);
    // cleanup anything that might have been modified by a test
    ShadowEnvironment.setExternalStorageState(Environment.MEDIA_UNMOUNTED);
  }
}
For a test to inherit this behavior, the ExtendedRobolectricTestRunner is used in place of the RobolectricTestRunner.

@RunWith(ExtendedRobolectricTestRunner.class)
public class FileUtilTest {
  // ... ...
}

This ensures that when a test executes, the Environment.getExternalStorageState returns MEDIA_MOUNTED by default; this simplifies the tests for any classes that may rely on FileUtil. The ExtendedRobolectricTestRunner is also where any custom shadows would be defined.

Robolectric provides powerful extension points that allow the behavior of classes to be further customized within your test suite. This is generally useful in cases where a class contains behavior that is difficult to test outside of an emulated environment. If you have developed classes for your application that fall into this category, it may be necessary to create shadows for these classes. For more details on creating custom shadows, refer to the Robolectric documentation.

Conclusion

The details covered here only provide the tip of the iceberg with respect to what can be customized and achieved with Robolectric. When you encounter a scenario in which writing a test is just not feasible, explore the APIs. Robolectric may have the support necessary to make it possible.

About

Jacob Orshalick is a software consultant, open source developer, speaker, and author. He is the owner of solutionsfit and co-author of the best-selling Seam Framework: Experience the Evolution of Java EE. His software development experience spans the...

1 comments
rabi328
rabi328

I know this post is a bit old, but I'm interested in this part  "Testing delayed and background tasks". I want to test that the background task last for 3 seconds for example. I've been googling for a couple of day, try mane different option, but none of them works works. Any suggestion?