Exploiting Android-Specific Seams for Testing and Flexibility

2017-01-21

As I pointed out throughout my series of posts on writing testable Android applications, the key to writing testable Android apps, is creating and exploiting seams. During these posts, I pointed out two types of seams that are available in any OO programming language and any programming environment. In this post, I want to highlight some Android-specific seams that we can leverage to make our applications more testable and flexible.

Manifest Seams

Manifest seams allow you to change a manifest that’s used for a particular build variant without editing your main manifest in place. Changing your manifest, of course, results in different behavior for your app. These new behaviors can be used to make your app more testable. To flesh out the concept of a manifest seam, let’s look at using manifest seams to set up your app’s “mock mode.”

An application running in mock mode stubs out its interactions with external services for testing purposes.1 A nice example of mock mode in action is Jake Wharton’s u2020 app. In his implementation of mock mode, Wharton uses an object seam, along with dagger, to swap out views within Activities so that in mock mode, the MainActivity’s view will contain a debug drawer that can be used to configure the stubbing behavior for the app.

public final class MainActivity extends Activity {

  @Inject ViewContainer viewContainer;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //...

    ViewGroup container = viewContainer.forActivity(this);    

    inflater.inflate(R.layout.main_activity, container);
    ButterKnife.bind(this, container);
    //...
  }  
}

This code gives u2020 users the ability to change mock behavior using the app’s UI without having to modify any production code. We could use manifest seams, however, to accomplish the same thing.2

Manifest seams are made possible via manifest merging and merge rule markers. To exploit a manifest seam for mock mode, we can use merge rule markers to tell the manifest merger to change the initial Activity that’s launched for a particular build variant.

<!-- src/mock/AndroidManifest.xml -->
<activity
  android:name=".StubConfigActivity">
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>
<activity
  android:name=".MainActivity">
  <intent-filter tools:node="remove">
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
</activity>

The resulting manifest for the mock build variant will have the StubConfigActivity as its launching activity, rather than the MainActivity.

I think there are other interesting possibilities here that are worth exploring. For example, you could use merge rule markers to substitute out which actions and categories are on non-launcher Activity’s intent-filter and thereby change which activities are started when we call context.startActivity with implicit intents.

BuildConfig Seams

BuildConfig seams allow us to change the values stored in the BuildConfig’s constants depending on which build variant we’re building. By default, BuildConfig contains some useful values like, DEBUG and FLAVOR, but we can actually create additional BuildConfig constants via gradle. Let’s start by looking a simple example of creating a BuildConfig field:

productFlavors {
  mock {
    buildConfigField('Boolean', 'MOCK', "true")
  }
}

A simple use case for BuildConfig seams is setting up the base url for the api your app is hitting. This can make testing easier because you can point your app to a staging, sandbox, mock, or production servers without modifying production code.3 Leveraging a BuildConfig seam in this way might look something like this:

defaultConfig {
  buildConfigField('String', 'API_BASE', '\"api.awesomecompany.com\"')
}
productFlavors {
  sandbox {
    buildConfigField('String', 'API_BASE', '\"localhost:8080\"')
  }
}

You could then use the BuildConfig field like this:

public String buildGetAwesomenessApiUrl() {
  return "http://" + BuildConfig.API_BASE + "/awesomenesss";
}

For those of you who have read my post on link seams, BuildConfig seams may just look like a special case of using build variants to create link seams. In some sense, that’s true.

However, BuildConfig seams have an advantage over placing identically named files in different sourcesets for different build variants: you can set a BuildConfig field for the default configuration and override it for specific build variants. You can’t do this by placing identically named files in the main and build-variant sourcesets folders because the compiler will complain that there’s two files with the same name.

There are other things to explore here as well. One use case I’ve found for BuildConfig seams is in composing dagger configuration behavior for multi-dimensional product flavors by storing class names in BuildConfig fields and instantiating them via reflection. I’m not very confident that this is a sensible way to use BuildConfig seams, but its interesting anyway, and it might serve as a foundation for a better way to use BuildConfig seams.

Resource Seams

Resources from different build variants, like AndroidManifests, get merged. Unlike manifest merging, we don’t have merge rule markers that allow us to tweak how the resources are merged. However, we can still take advantage of the default merge behavior to change the behavior of our apps without editing production code in place. The default merge behavior, according to the docs, is this:

build variant > build type > product flavor > main source set > library dependencies

This means that we can place resources in the main source set as a kind of default and override them for specific build variants. Again, this is something that we can’t do by placing identically named java files in different sourcesets.

BuildConfig seams and Resource seams obviously have some similarities, which makes choosing between them confusing. Off the cuff, we can say that using a BuildConfig field is easier than getting a resource value, so we may want to prefer BuildConfig seams when we don’t have access to a Context. Stuffing all of our build variant specific values into a single BuildConfig class, however, isn’t going to scale well, so we may want to prefer Resource seams if we do have access to a Context.

Conclusion

Android developers have their own Android-specific seams that they can exploit for testing purposes. Manifest seams rely on manifest merging and merge rule markers. BuildConfig seams rely on the productFlavor.buildConfigField method. Resource seams rely on Android’s default the resource merging behavior.

Notes:

  1. If you don’t know why we’d want to do this, read this.

  2. Manifest seams may be an inferior way of doing this, as Wharton’s strategy allows users to change mock behavior throughout their session with the app, while manifest seams, as we’ll see, only allow us to change this behavior when the app is first launched. My purpose here isn’t to say which approach is better. Its just to point out that manifest seams exist.

  3. I haven’t found much use for mock web servers. I can eliminate flaky tests without them, and they often slow tests down since the tests are still making network requests.

androidtesting

Espresso Test Addiction: An Anti-pattern

TDD > The Principle of Single Responsibility

comments powered by Disqus