Loading...
  • Microsoft’s MDM solution solves SMB and enterprise needs
    Microsoft’s MDM solution solves SMB and enterprise needs
  • Evaluating peripheral device management software
    Evaluating peripheral device management software
  •  Mobile app revolution means new management tools for IT
    Mobile app revolution means new management tools for IT
  • How to use IP Address Management in Windows Server 2012
    How to use IP Address Management in Windows Server 2012
  • What SDN means to the network administrator
    What SDN means to the network administrator
Software Engineer

Unit testing Android apps with Robolectric: advanced use cases

unit-testing-android-apps

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Show Buttons
Hide Buttons