I've added a new method to a system service in Android 9
There is a system library, which provides an API for developers and uses modified framework. Library works OK and successfully accesses the new method.
I want to test the library using Robolectric.
#########################
# Robolectric test target
#########################
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-test \
mockito-robolectric-prebuilt \
truth-prebuilt
LOCAL_JAVA_LIBRARIES := \
junit \
platform-robolectric-3.6.1-prebuilt
LOCAL_JAVA_LIBRARIES += com.mars.thelibrary
LOCAL_INSTRUMENTATION_FOR := TheService
LOCAL_MODULE := TheServiceRoboTests
LOCAL_MODULE_TAGS := optional
include $(BUILD_STATIC_JAVA_LIBRARY)
###########################
# Robolectric runner target
###########################
include $(CLEAR_VARS)
LOCAL_MODULE := RunTheServiceRoboTests
LOCAL_SDK_VERSION := current
LOCAL_STATIC_JAVA_LIBRARIES := TheServiceRoboTests
LOCAL_TEST_PACKAGE := TheService
LOCAL_JAVA_LIBRARIES += com.mars.thelibrary
include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk
// TestConfig.java
public class TestConfig {
public static final int SDK_VERSION = 26;
public static final String MANIFEST_PATH = "TheAwesomeLibrary/tests/robotests/AndroidManifest.xml";
}
// PackageUtilitiesTest.java
@RunWith(RobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class PackageUtilitiesTest {
private Installer installer; // Modified System Service
private PackageUtilities utilities; // Library Class
@Before
public void setUp() throws InstallerException {
installer = mock(Installer.class);
utilities = new PackageUtilities(installer);
}
@Test
public void disableApp_shouldSuspendCache() throws InstallerException {
// Prepare
doNothing()
.when(installer)
.suspendCache(anyString(), anyInt());
// Do
utilities.disableApp("com.mars.betrayer", true);
// Check
verify(installer, times(1)).suspendCache(anyString(), anyInt());
}
}
On Prepare stage it fails with an error
java.lang.NoSuchMethodError: com.android.server.pm.Insteller.suspendCache(Ljava/lang/String;I)V
But I'm pretty sure that the method exists, because the library works correctly on a device.
In all other Robolectric tests the library works with unmodified framework parts, so all other tests work and pass as expected.
How to write robolectric tests properly? How to make them work with the modified framework?
Two reasons:
For now, sdk version should be the same as the platform's version. E.g. for Android 9:
@Config(sdk = Build.VERSION_CODES.P)
(still relevant for android-12.0.0_r4)
BUT
This won't work, and you'll see a new error:
java.lang.UnsupportedOperationException: Robolectric does not support API level 28.
To avoid that, you have to use robolectric from external/robolectric-shadows
rather than prebuilts/misc/common/robolectric
Change
include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk
To
include external/robolectric-shadows/run_robotests.mk
And change dependencies like this (mind the LOCAL_JAVA_LIBRARIES
declarations):
#########################
# Robolectric test target
#########################
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := \
mockito-robolectric-prebuilt \
junit \
Robolectric_all-target \
LOCAL_INSTRUMENTATION_FOR := TheService
LOCAL_MODULE := TheServiceRoboTests
LOCAL_MODULE_TAGS := optional
include $(BUILD_STATIC_JAVA_LIBRARY)
###########################
# Robolectric runner target
###########################
include $(CLEAR_VARS)
LOCAL_MODULE := RunTheServiceRoboTests
LOCAL_SDK_VERSION := current
LOCAL_STATIC_JAVA_LIBRARIES := TheServiceRoboTests
LOCAL_TEST_PACKAGE := TheService
LOCAL_JAVA_LIBRARIES := \
TheServiceRoboTests \
guava \
Robolectric_all-target \
robolectric_android-all-stub \
mockito-robolectric-prebuilt \
include external/robolectric-shadows/run_robotests.mk
In future there will be another way, which will help not to specify the exact version every release anymore:
@Config(sdk = Build.VERSION_CODES.CUR_DEVELOPMENT)
See 1819020: Makes a special entry for tree-built Robolectric
When you set the SDK version
@Config(sdk = 26)
Then robolectric will try to find it in a map of supported SDKs in robolectric: SdkConfig.java
new HashMap<Integer, SdkVersion>() {
{
addSdk(Build.VERSION_CODES.JELLY_BEAN, "4.1.2_r1", "r1", "REL");
addSdk(Build.VERSION_CODES.JELLY_BEAN_MR1, "4.2.2_r1.2", "r1", "REL");
addSdk(Build.VERSION_CODES.JELLY_BEAN_MR2, "4.3_r2", "r1", "REL");
addSdk(Build.VERSION_CODES.KITKAT, "4.4_r1", "r2", "REL");
addSdk(Build.VERSION_CODES.LOLLIPOP, "5.0.2_r3", "r0", "REL");
addSdk(Build.VERSION_CODES.LOLLIPOP_MR1, "5.1.1_r9", "r2", "REL");
addSdk(Build.VERSION_CODES.M, "6.0.1_r3", "r1", "REL");
addSdk(Build.VERSION_CODES.N, "7.0.0_r1", "r1", "REL");
addSdk(Build.VERSION_CODES.N_MR1, "7.1.0_r7", "r1", "REL");
addSdk(Build.VERSION_CODES.O, "8.0.0_r4", "r1", "REL");
addSdk(Build.VERSION_CODES.O_MR1, "8.1.0", "4611349", "REL");
addSdk(Build.VERSION_CODES.P, "9", "4913185-2", "REL");
addSdk(Build.VERSION_CODES.Q, "10", "5803371", "REL");
// BEGIN-INTERNAL
// TODO: Update jar with final R release.
addSdk(Build.VERSION_CODES.R, "R-beta2", "6625208", "REL");
addSdk(Build.VERSION_CODES.S, "S", "r0", "S");
// END-INTERNAL
}
In AOSP, Robolectric loads the Framework as a .jar
file. When Runner is starting the test, it then looks up for an appropriate supported jar.
It matches 28 with Build.VERSION_CODES.O
, takes the Sdk(Build.VERSION_CODES.O, "8.0.0_r4", "r1", "REL")
, and uses it to build a name of the jar
public DependencyJar getAndroidSdkDependency() {
return createDependency("org.robolectric", "android-all", getSdkVersion().getAndroidVersion() + "-robolectric-" + getSdkVersion().getRobolectricVersion(), null);
}
For example, for Android 8 (26; Build.VERSION_CODES.O) it will try to use android-all-8.0.0_r4-robolectric-r1.jar
of already released android version.
So, when the test is launched by run_robotests.mk
, here it copies all existing jar files from prebuilts/misc/common/robolectric/android-all and adds it to its place inside the out/
directory for future usage.
This jar is already have all classes and methods, and it won't contain anything new
To get all latest changes that you've made to the framework, you have to pass your current sdk verion. If your AOSP is Android 12, then you have to declare your test like this
@Config(sdk = Build.VERSION_CODES.S)
When it gets the latest version, it then uses Sdk(Build.VERSION_CODES.S, "S", "r0", "S");
, searches for android-all-s-robolectric-r0.jar
, copies and uses it.
Unlike other versions, this jar will be built from the current sources tree, and thus the new changes will be seen in Robolectric tests.