How to run Android Instrumentation tests with ProGuard

Barend Garvelink

The app I'm working on has an extensive, automated test suite built using Instrumentation and Robotium. These are white-box tests; they know about the Java classes that make up the tested app. This means that we were unable to run the tests after ProGuarding our application. I've never seen ProGuard break any working code after setting up the right exclusions. Nevertheless, it's a bit questionable to have a huge automated test suite and not run it on the APK that we deliver to the Play Store.

ProGuard and Instrumentation Tests

One possible work-around is to use the UIAutomator framework, which does not have this limitation. There are several reason's why we couldn't: it requires API level 16 (Jelly Bean), our app supports API 8 (Froyo) as its minimum SDK version. Furthermore, we already had our test suite in place and rewriting it from scratch didn't seem like a very good use of time. Here's what we did instead.

The basic idea couldn't be simpler. Can't run the tests on an obfuscated package? Obfuscate the tests the same way! ProGuard has the -applymapping directive, which works in conjunction with the -printmapping directive to let you re-use an obfuscation mapping. If you're using the standard Android Ant build scripts, you need to change a couple of things:

Update the ProGuard config of the main project

If you didn't already add it for retrace, add a -printmapping directive to the ProGuard config of your main project.

# Generates bin/proguard-mapping.txt
-printmapping proguard-mapping.txt

Update the ProGuard config of the test project

Your test project needs two things: the -applymapping directive to reuse the main project's mapping file and as many -injars directives as needed to add all the plain versions of your program code and libraries.

# Reuse the main project's ProGuard mapping
-applymapping ../mainproject/bin/proguard-mapping.txt

# ProGuard is automatically fed the obfuscated code of the main
# project. It also needs the plain version:
-injars ../mainproject/bin/classes
-injars ../mainproject/libs/android-support-v4.jar

You may have to add some -dontwarn directives. I got warned about a missing finish() method on my activites, which will never be a problem at run time 'cause it's in the base class.

Make sure your tests are suitable

Some of our Robotium tests referred to activity classes by their simple name, e.g.:

  private void goBackBackBackUntilHome() {
    while ( !"MainActivity".equals(solo.getCurrentActivity().getClass().getSimpleName()) ) {
      // sleep a bit and guard against infinite loop
      solo.goBack();
    }
  }

ProGuard will not recognize this as a class reference and will not transform it. Either of the following alternatives will work:

  // Using a class literal
  private void goBackBackBackUntilHome() {
    while ( !MainActivity.class.isInstance(solo.getCurrentActivity()) ) {
      // sleep a bit and guard against infinite loop
      solo.goBack();
    }
  }

  // Using a fully qualified class name
  private void goBackBackBackUntilHome2() {
    while ( !"com.myapp.MainActivity".equals(solo.getCurrentActivity().getClass().getName()) ) {
      // sleep a bit and guard against infinite loop
      solo.goBack();
    }
  }

Furthermore, if you're logging any activities' class names to help you diagnose any test failures, you may now want to also log their titles.

Enable code signing

Both APK files have to be signed with the same key to allow the tests to run. You could use your actual release key, but I would advise against using that key so casually. Instead, configure your project to use the Android debug key. When it comes time to publish a release to the Play Store, simply strip the existing signature from the APK file and re-sign, re-align with your production key.

To sign with the debug key, add the following four lines to the ant.properties file in both the main project and the test project:

key.store=${user.home}/.android/debug.keystore
key.alias=androiddebugkey
key.store.password=android
key.alias.password=android

Get ant to properly chain the projects

When you run any build target on the test project, it'll automatically delegate to the tested project and build that too. Unfortunately, there's an oversight in Google's build file (sdk rev 22 / tools rev 18). When you run a release build of the test project, it goes ahead and does a debug build of the main project. You can easily work around this with a little ant property abuse:

user@machine test-project$ ant release -Dtested.project.target=release

This causes both projects to be built in release mode and the whole setup will now work.

Bonus tip: use -Dtested.project.target=help to skip building the main project if you've only changed your tests.

Run your tests

I'm actually not sure whether, after all this, the ant test command still works. We already had shell scripts in place that invoke adb install and adb shell am instrument directly instead of through Ant. You may need something similar.

Comments (4)

  1. K76154 - Reply

    December 3, 2013 at 5:15 pm

    Does this work even if I reference to R.id and R.string in my instrumentation test?

  2. Thomas - Reply

    October 14, 2014 at 1:09 pm

    Hi Barend! This solution seems to work with Ant-based builds, but have you ever come around and tried this with a Gradle-based build? Since the instrumentation tests are now part of the main project, I am unable to configure a different set of proguard rules just for the test app... Thanks, Thomas.

  3. Thomas - Reply

    October 15, 2014 at 12:36 am

    Hi Barend! I created a feature request for that earlier already - https://code.google.com/p/android/issues/detail?id=77475 - because I thought this might be an unsupported use case. I linked the thread you mentioned to this bug and vice versa. Feel free to star / comment on that. Thanks, Thomas.

Add a Comment