< Instrumentation

Creating a component for testing

Creating a component, e.g. a ContentProvider or Service, for use in testing can be tricky. This is particularly true when:

  • app code and test code are in separate APKs
  • the test APK instruments the app APK
  • the test Service depends on app code.

This doc explains the pitfalls in creating a test component and how to avoid them.

What can go wrong with a test component

tl;dr: test components may not be able to access app code when defined in the test APK.

Chromium‘s instrumentation test suites are all currently set up in the manner described above: app code is in one APK (the APK under test), test code is in another APK (the test APK), and auxiliary code, when necessary, is in one or more other APKs (support APKs). Test APKs build against app code but do not retain it in their .dex files. This reduces the size of test APKs and avoids potentially conflicting definitions. At instrumentation runtime, the test code is loaded into the app package’s process, and it‘s consequently able to access code defined in the app APK’s .dex file(s). Test components, however, run in the test package's process and only have access to code in the test APK. While test components referencing app code will build without issue, they will fail to link at runtime and will consequently not be able to be instantiated.

For example, here‘s the logcat from an attempt to use a test Service, TestPostMessageService, that extended an app Service, PostMessageService. Note that the runtime link failed because the superclass couldn’t be resolved, and the test Service could not be instantiated as a result.

Unable to resolve superclass of Lorg/chromium/chrome/browser/customtabs/TestPostMessageService; (184)
Link of class 'Lorg/chromium/chrome/browser/customtabs/TestPostMessageService;' failed
...
FATAL EXCEPTION: main
Process: org.chromium.chrome.tests, PID: 30023
java.lang.RuntimeException:
    Unable to instantiate service org.chromium.chrome.browser.customtabs.TestPostMessageService:
        java.lang.ClassNotFoundException:
        Didn't find class "org.chromium.chrome.browser.customtabs.TestPostMessageService" on path:
        DexPathList[
            [zip file "/system/framework/android.test.runner.jar",
             zip file "/data/app/org.chromium.chrome.tests-1.apk"],
            nativeLibraryDirectories=[
                /data/app-lib/org.chromium.chrome.tests-1,
                /vendor/lib,
                /system/lib]]
  at android.app.ActivityThread.handleCreateService(ActivityThread.java:2543)
  at android.app.ActivityThread.access$1800(ActivityThread.java:135)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:136)
  at android.app.ActivityThread.main(ActivityThread.java:5001)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:515)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
  at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ClassNotFoundException:
    Didn't find class "org.chromium.chrome.browser.customtabs.TestPostMessageService" on path:
    DexPathList[
        [zip file "/system/framework/android.test.runner.jar",
         zip file "/data/app/org.chromium.chrome.tests-1.apk"],
        nativeLibraryDirectories=[
            /data/app-lib/org.chromium.chrome.tests-1,
            /vendor/lib,
            /system/lib]]
  at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:497)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:457)
  at android.app.ActivityThread.handleCreateService(ActivityThread.java:2540)
  ... 10 more

How to implement a test component

There are (at least) two mechanisms for avoiding the failures described above: using a support APK or using sharedUserIds.

Use a support APK

Putting the Service in a support APK lets the build system include all necessary code in the .dex without fear of conflicting definitions, as nothing in the support APK runs in the same package or process as the test or app code.

To do this:

Create the component.

It should either be in an existing directory used by code in the appropriate support APK or a new directory for such purpose. In particular, it should be in neither a directory containing app code nor a directory containing test code.

package org.chromium.chrome.test;

import org.chromium.chrome.MyAppService;

public class MyTestService extends MyAppService {
    ...
}

Put the component in a separate gn target.

This can either be a target upon which the support APK already depends or a new target.

android_library("my_test_service") {
  java_files = [ "src/org/chromium/chrome/test/MyTestService.java" ]
  deps = [ ... ]
}

The support APK must depend on this target. The target containing your test code can also depend on this target if you want to refer to the component class directly, e.g., when binding a Service.

NOTE: Even if your test code directly depends on the service target, you won‘t be able to directly reference the test component instance from test code. Checking a test component’s internal state will require adding APIs specifically for that purpose.

Define the component in the support APK manifest.

<manifest ...
    package="org.chromium.chrome.tests.support" >

  <application>
    <service android:name="org.chromium.chrome.test.MyTestService"
        android:exported="true"
        tools:ignore="ExportedService">
      ...
    </service>

    ...
  </application>
</manifest>

Use a sharedUserId

Using a sharedUserId will allow your component to run in your app's process.

Because this requires modifying the app manifest, it is not recommended at this time and is intentionally not further documented here.