Makes it possible to robo-test Settings app fragments.

This adds bunch of shadow/placeholder classes and logic to handle
references to Android internal resources or newly added classes/methods
that Robolectric hasn't yet picked up.

Developers can follow ManageApplicationsTest example to use the shadow
classes and the utility method to start ther fragment in their
robolectric tests.

Bug: 33431346
Test: This is a test improvement CL. RunSettingsRoboTests still passes.
Change-Id: I943ab871631cb8c368d9f9db481c00558c5c4d1f
This commit is contained in:
Jaewoong Jung
2016-11-30 15:45:53 -08:00
parent 276e3b7a5e
commit bccd652503
7 changed files with 261 additions and 2 deletions

View File

@@ -87,4 +87,5 @@ com.android.settings.applications.AppStorageSettings
com.android.settings.notification.NotificationAccessSettings
com.android.settings.notification.ZenModeSettings
com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment
com.android.settings.applications.ConvertToFbe
com.android.settings.applications.ConvertToFbe
com.android.settings.localepicker.LocaleListEditor

View File

@@ -0,0 +1,19 @@
package android.print;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Loader;
import android.printservice.PrintServiceInfo;
import com.android.internal.util.Preconditions;
import java.util.List;
/**
* A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility.
*/
public class PrintServicesLoader extends Loader<List<PrintServiceInfo>> {
public PrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context,
int selectionFlags) {
super(Preconditions.checkNotNull(context));
}
}

View File

@@ -0,0 +1,11 @@
package com.android.internal.app;
/**
* A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility.
*/
public class LocalePickerWithRegion {
public interface LocaleSelectedListener {
void onLocaleSelected(LocaleStore.LocaleInfo locale);
}
}

View File

@@ -15,15 +15,23 @@
*/
package com.android.settings;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.Fs;
import org.robolectric.res.ResourcePath;
import org.robolectric.util.ActivityController;
import org.robolectric.util.ReflectionHelpers;
import java.util.List;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT;
import static org.robolectric.Robolectric.getShadowsAdapter;
/**
* Custom test runner for the testing of BluetoothPairingDialogs. This is needed because the
* default behavior for robolectric is just to grab the resource directory in the target package.
@@ -77,4 +85,15 @@ public class SettingsRobolectricTestRunner extends RobolectricTestRunner {
manifest.setPackageName("com.android.settings");
return manifest;
}
}
// A simple utility class to start a Settings fragment with an intent. The code here is almost
// the same as FragmentTestUtil.startFragment except that it starts an activity with an intent.
public static void startSettingsFragment(
Fragment fragment, Class<? extends SettingsActivity> activityClass) {
Intent intent = new Intent().putExtra(EXTRA_SHOW_FRAGMENT, fragment.getClass().getName());
SettingsActivity activity = ActivityController.of(
getShadowsAdapter(), ReflectionHelpers.callConstructor(activityClass), intent)
.setup().get();
activity.getFragmentManager().beginTransaction().add(fragment, null).commit();
}
}

View File

@@ -0,0 +1,61 @@
package com.android.settings.applications;
import android.os.Looper;
import android.os.UserManager;
import com.android.settings.Settings;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.testutils.shadow.ShadowDynamicIndexableContentMonitor;
import com.android.settings.testutils.shadow.SettingsShadowResources;
import com.android.settings.testutils.shadow.SettingsShadowResources.SettingsShadowTheme;
import com.android.settingslib.applications.ApplicationsState;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
/**
* Tests for {@link ManageApplications}.
*/
@RunWith(SettingsRobolectricTestRunner.class)
// TODO: Consider making the shadow class set global using a robolectric.properties file.
@Config(manifest = TestConfig.MANIFEST_PATH,
sdk = TestConfig.SDK_VERSION,
shadows = {
SettingsShadowResources.class,
SettingsShadowTheme.class,
ShadowDynamicIndexableContentMonitor.class
})
public class ManageApplicationsTest {
@Mock private ApplicationsState mState;
@Mock private ApplicationsState.Session mSession;
@Mock private UserManager mUserManager;
private Looper mBgLooper;
private ManageApplications mFragment;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
ReflectionHelpers.setStaticField(ApplicationsState.class, "sInstance", mState);
when(mState.newSession(any())).thenReturn(mSession);
mBgLooper = Looper.myLooper();
when(mState.getBackgroundLooper()).thenReturn(mBgLooper);
mFragment = new ManageApplications();
}
@Test
public void launchFragment() {
SettingsRobolectricTestRunner.startSettingsFragment(
mFragment, Settings.ManageApplicationsActivity.class);
}
}

View File

@@ -0,0 +1,126 @@
package com.android.settings.testutils.shadow;
import android.annotation.DimenRes;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.res.StyleData;
import org.robolectric.res.StyleResolver;
import org.robolectric.res.builder.XmlResourceParserImpl;
import org.robolectric.shadows.ShadowAssetManager;
import org.robolectric.shadows.ShadowResources;
import org.robolectric.util.ReflectionHelpers;
import org.w3c.dom.Node;
import java.util.List;
import java.util.Map;
import static android.util.TypedValue.TYPE_REFERENCE;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.internal.Shadow.directlyOn;
/**
* Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot
* handle because they are too new or private.
*/
@Implements(Resources.class)
public class SettingsShadowResources extends ShadowResources {
@RealObject Resources realResources;
@Implementation
public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
// Handle requests for private dimension resources,
// TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
return 0;
}
return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
}
@Implementation
public Drawable loadDrawable(TypedValue value, int id, Theme theme)
throws NotFoundException {
// The drawable item in switchbar_background.xml refers to a very recent color attribute
// that Robolectric isn't yet aware of.
// TODO: Remove this once Robolectric is updated.
if (id == com.android.settings.R.drawable.switchbar_background) {
return new ColorDrawable();
}
return super.loadDrawable(value, id, theme);
}
@Implements(Theme.class)
public static class SettingsShadowTheme extends ShadowTheme {
@RealObject
Theme realTheme;
@Implementation
public TypedArray obtainStyledAttributes(
AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
// Replace all private string references with a placeholder.
if (set != null) {
for (int i = 0; i < set.getAttributeCount(); ++i) {
if (set.getAttributeValue(i).startsWith("@*android:string")) {
Node node = ReflectionHelpers.callInstanceMethod(
XmlResourceParserImpl.class, set, "getAttributeAt",
ReflectionHelpers.ClassParameter.from(int.class, i));
node.setNodeValue("PLACEHOLDER");
}
}
}
// Track down all styles and remove all inheritance from private styles.
ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets());
// The Object's below are actually ShadowAssetManager.OverlayedStyle. We can't use it
// here because it's package private.
Map<Long, List<Object>> appliedStylesList =
ReflectionHelpers.getField(assetManager, "appliedStyles");
for (Long idx : appliedStylesList.keySet()) {
List<Object> appliedStyles = appliedStylesList.get(idx);
int i = 1;
for (Object appliedStyle : appliedStyles) {
StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle, "style");
List<StyleData> styleDatas =
ReflectionHelpers.getField(styleResolver, "styles");
for (StyleData styleData : styleDatas) {
if (styleData.getParent() != null &&
styleData.getParent().startsWith("@*android:style")) {
ReflectionHelpers.setField(StyleData.class, styleData, "parent", null);
}
}
}
}
return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}
@Implementation
public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) {
// The real Resources instance in Robolectric tests somehow fails to find the
// preferenceTheme attribute in the layout. Let's do it ourselves.
if (getResources().getResourceName(resid)
.equals("com.android.settings:attr/preferenceTheme")) {
int preferenceThemeResId =
getResources().getIdentifier(
"PreferenceTheme", "style", "com.android.settings");
outValue.type = TYPE_REFERENCE;
outValue.data = preferenceThemeResId;
outValue.resourceId = preferenceThemeResId;
return true;
}
return directlyOn(realTheme, Theme.class)
.resolveAttribute(resid, outValue, resolveRefs);
}
}
}

View File

@@ -0,0 +1,22 @@
package com.android.settings.testutils.shadow;
import android.app.Activity;
import android.os.UserManager;
import com.android.settings.search.DynamicIndexableContentMonitor;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
/**
* A shadow class of {@link DynamicIndexableContentMonitor}. The real implementation of
* {@link DynamicIndexableContentMonitor#register} calls {@link UserManager#isUserUnlocked()}, which
* Robolectric has not yet been updated to support, so throws a NoSuchMethodError exception.
*/
// TODO: Delete this once Robolectric is updated to the latest SDK.
@Implements(DynamicIndexableContentMonitor.class)
public class ShadowDynamicIndexableContentMonitor {
@Implementation
public void register(Activity activity, int loaderId) {
}
}