Last active
April 27, 2020 09:30
-
-
Save OleksandrKucherenko/90ed909a2a1b0411b6c6de8798f5a7fe to your computer and use it in GitHub Desktop.
Robolectric full lifecycle of activity/fragment looper.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package net.easypark.junit; | |
import android.annotation.TargetApi; | |
import android.app.Activity; | |
import android.app.Application; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.os.Build; | |
import android.os.Bundle; | |
import android.support.annotation.*; | |
import android.support.v4.app.Fragment; | |
import android.support.v4.util.Pair; | |
import android.support.v7.app.AppCompatActivity; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.widget.FrameLayout; | |
import net.easypark.android.R; | |
import net.easypark.android.utils.Logs; | |
import org.robolectric.Robolectric; | |
import org.robolectric.RuntimeEnvironment; | |
import org.robolectric.android.controller.ActivityController; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import java.util.Stack; | |
import rx.functions.Action0; | |
import rx.functions.Action1; | |
import rx.functions.Func0; | |
import rx.functions.Func2; | |
/** Helper class that allows to emulate lifecycle for fragment inside the isolated activity. */ | |
@SuppressWarnings("unused") | |
public class RobolectricHelper { | |
/** Class logger. */ | |
public static final Logs _log = Logs.of(RobolectricHelper.class); | |
private static final String SEPARATOR = new String(new char[10]).replace('\0', '#') + " "; | |
//region Activity Lifecycle Emulation | |
/** Run full lifecycle emulation with */ | |
public static <T extends Activity> ActivityController<T> fullLifecycle( | |
@NonNull final ActivityController<T> controller, | |
@NonNull final Action0 onVisible) { | |
return fullLifecycle(controller, null, null, RobolectricHelper.<T>up(onVisible), null); | |
} | |
/** | |
* Run full lifecycle emulation with re-creation of activity instance. | |
* Re-create is often needed for testing retain fragments state set by {@link Fragment#setRetainInstance(boolean)} call. | |
* <p> | |
* <pre> | |
* ActivityController<TActivity<Login>> controller = RobolectricHelper.testFragment(Login.class); | |
* TActivity<Login> activity = controller.get(); | |
* Action0 validation = new Action0(){ ... }; | |
* | |
* RobolectricHelper.fullLifecycle(controller, validation, activity.reCreator()); | |
* </pre> | |
*/ | |
public static <T extends Activity> ActivityController<T> fullLifecycle( | |
@NonNull final ActivityController<T> controller, | |
@NonNull final Action0 onVisible, | |
@NonNull final Func0<ActivityController<T>> onActivityRecreate) { | |
return fullLifecycle(controller, null, null, RobolectricHelper.<T>up(onVisible), onActivityRecreate); | |
} | |
/** Run full lifecycle with recreation of the activity and fragment. */ | |
public static <F extends Fragment> ActivityController<TActivity<F>> fullLifecycle( | |
@NonNull final Help<F> helper, | |
@NonNull final Action1<TActivity<F>> onVisible) { | |
return fullLifecycle(helper.controller, onVisible, helper.reCreator); | |
} | |
/** | |
* Run full lifecycle emulation with re-creation of activity instance. | |
* Re-create is often needed for testing retain fragments state set by {@link Fragment#setRetainInstance(boolean)} call. | |
* <p> | |
* <pre> | |
* ActivityController<TActivity<Login>> controller = RobolectricHelper.testFragment(Login.class); | |
* TActivity<Login> activity = controller.get(); | |
* Action1<Login> validation = new Action1(){ ... }; | |
* | |
* RobolectricHelper.fullLifecycle(controller, validation, activity.reCreator()); | |
* </pre> | |
*/ | |
public static <T extends Activity> ActivityController<T> fullLifecycle( | |
@NonNull final ActivityController<T> controller, | |
@NonNull final Action1<T> onVisible, | |
@NonNull final Func0<ActivityController<T>> onActivityRecreate) { | |
return fullLifecycle(controller, null, null, onVisible, onActivityRecreate); | |
} | |
/** | |
* Perform full lifecycle emulation for activity. When Activity is in visible state is possible to execute some | |
* additional actions. | |
* | |
* @param onRecreate provide instance if you want to test recreation of the activity, otherwise NULL. | |
*/ | |
public static <T extends Activity> ActivityController<T> fullLifecycle( | |
@NonNull ActivityController<T> controller, | |
@Nullable final Action1<T> onRestart, | |
@Nullable final Action1<T> onResume, | |
@Nullable final Action1<T> onVisible, | |
@Nullable final Func0<ActivityController<T>> onRecreate) { | |
Bundle savedInstanceState = null; | |
// do recreate only if defined callback | |
int recreateLoops = (null != onRecreate) ? 1 : 0; | |
do { | |
_log.info("enter - onCreate : %s", recreateLoops); | |
controller.create(savedInstanceState); | |
_log.info("state - onCreate"); | |
// CYCLE #1: emulate activity restart | |
int lifeLoops = 1; | |
do { | |
_log.info("enter - onStart : %s", lifeLoops); | |
controller.start(); | |
_log.info("state - onStart"); | |
if (null != savedInstanceState) { | |
_log.debug("saved state - recovery"); | |
controller.restoreInstanceState(savedInstanceState); | |
controller.postCreate(savedInstanceState); | |
} | |
// CYCLE #1.1: emulate show/hide | |
int loops = 1; | |
do { | |
_log.info("enter - onResume : %s", loops); | |
if (null != onResume) onResume.call(controller.get()); | |
controller.resume(); // --> onPostResume() | |
_log.info("state - onResume"); | |
// TODO: not implemented onAttachedToWindow() call | |
controller.visible(); // --> onCreateOptionsMenu(), onUserInteraction() | |
if (null != onVisible) onVisible.call(controller.get()); | |
controller.userLeaving(); | |
_log.info("enter - onPause"); | |
controller.pause(); | |
_log.info("state - onPause"); | |
loops--; | |
} while (loops >= 0); | |
// CHECK-ME: robolectric call it before #pause() | |
controller.saveInstanceState(savedInstanceState = new Bundle()); | |
_log.verbose("%s", savedInstanceState); | |
_log.info("enter - onStop"); | |
controller.stop(); | |
_log.info("state - onStop"); | |
// TODO: not implemented onRetainNonConfigurationInstance() --> controller.get().onRetainNonConfigurationInstance(); | |
// go-to onRestart() state | |
if (lifeLoops > 0) { | |
if (null != onRestart) onRestart.call(controller.get()); | |
// during restart we do not need the savedInstanceState, drop the instance | |
savedInstanceState = null; | |
_log.debug("saved state - dropped"); | |
_log.info("enter - onRestart"); | |
controller.restart(); | |
_log.info("state - onRestart"); | |
} | |
lifeLoops--; | |
} while (lifeLoops >= 0); | |
_log.info("enter - onDestroy"); | |
controller.destroy(); | |
_log.info("state - onDestroy"); | |
// save instance and recover it for additional lifecycle loop | |
if (recreateLoops > 0) { | |
if (null != onRecreate) { | |
controller = onRecreate.call(); | |
} | |
_log.info("%sstate - recreate : %s", SEPARATOR, recreateLoops); | |
// controller.attach(); | |
} | |
recreateLoops--; | |
} while (recreateLoops >= 0); | |
return controller; // can be a new instance due to re-create execution | |
} | |
//endregion | |
//region Test Fragment in robolectric test activity | |
/** Create test activity that will contain our fragment. */ | |
@NonNull | |
public static <T extends Fragment> ActivityController<TActivity<T>> testFragment(@NonNull final Class<T> clazz) { | |
return testFragment(clazz, null); | |
} | |
/** Create test activity that will contain our fragment with additional parameters. */ | |
@SuppressWarnings("unchecked") | |
@NonNull | |
public static <T extends Fragment> ActivityController<TActivity<T>> testFragment( | |
@NonNull final Class<T> clazz, | |
@Nullable final Bundle saved) { | |
final Application application = RuntimeEnvironment.application; | |
final Intent intent = new Intent(application, clazz); | |
if (null != saved) intent.putExtras(saved); | |
final Class<? extends TActivity<T>> activity = hostClazz(clazz); | |
return (ActivityController<TActivity<T>>) Robolectric.buildActivity(activity, intent); | |
} | |
//endregion | |
//region Helpers | |
/** Print into log activity views hierarchy. */ | |
@NonNull | |
public static String logViewHierarchy(@NonNull final Activity activity) { | |
return logViewHierarchy(activity.findViewById(android.R.id.content)); | |
} | |
/** Print into log view hierarchy. */ | |
@NonNull | |
public static String logViewHierarchy(@NonNull final View root) { | |
final StringBuilder output = new StringBuilder(8192).append("\n"); | |
final Stack<Pair<String, View>> stack = new Stack<>(); | |
stack.push(Pair.create("", root)); | |
boolean nextLevel = false; | |
while (!stack.empty()) { | |
final Pair<String, View> p = stack.pop(); | |
final View v = p.second; | |
final boolean isLastOnLevel = stack.empty() || !p.first.equals(stack.peek().first); | |
final String graphics = "" + p.first + (isLastOnLevel ? "└── " : "├── "); | |
final String className = v.getClass().getSimpleName(); | |
final String line = graphics + className + " id=" + v.getId(); | |
output.append(line).append("\n"); | |
if (v instanceof ViewGroup) { | |
final ViewGroup vg = (ViewGroup) v; | |
for (int i = vg.getChildCount() - 1; i >= 0; i--) { | |
stack.push(Pair.create(p.first + (isLastOnLevel ? " " : "│ "), vg.getChildAt(i))); | |
} | |
} | |
} | |
final String msg = output.toString(); | |
// dump results | |
_log.v(msg); | |
return msg; | |
} | |
/** Extract class type from activity instance. */ | |
@SuppressWarnings({"unchecked", "rawtypes"}) | |
@NonNull | |
private static <T extends Fragment> Class<? extends TActivity<T>> hostClazz(@NonNull final Class<T> clazz) { | |
final Class<? extends TActivity> instanceClazz = (new TActivity<>(clazz)).getClass(); | |
return (Class<? extends TActivity<T>>) instanceClazz; | |
} | |
/** Upgrade Action0 instance to Action1. */ | |
@NonNull | |
private static <T> Action1<T> up(@NonNull final Action0 caller) { | |
return new Action1<T>() { | |
@Override | |
public void call(final T t) { | |
caller.call(); | |
} | |
}; | |
} | |
//endregion | |
//region Nested declarations | |
/** Hosting activity that can be used for any fragment testing. */ | |
public static class TActivity<T extends Fragment> extends AppCompatActivity { | |
/** Tag for easier finding of the fragment. */ | |
public static final String TAG_TEST_FRAGMENT = "TAG_TEST_FRAGMENT"; | |
/** unique id used for testing. */ | |
public static final int ID = R.id.home; | |
/** Layout Params that ask for matching the parent size. */ | |
public static final ViewGroup.LayoutParams MATCH_PARENT = new ViewGroup.LayoutParams( | |
ViewGroup.LayoutParams.MATCH_PARENT, | |
ViewGroup.LayoutParams.MATCH_PARENT); | |
/** Cache of the instance. */ | |
private T mInstance; | |
/** Configuration action. Allows to apply configuration on fragment instance during it creation. */ | |
/* package */ Action1<T> mConfiguration; | |
/** Creator function, can be used for overriding default implementation of fragment instance creation. */ | |
/* package */ Func2<Context, Intent, T> mCreator = new DoDefaultOs<>(); | |
/** Fragment data type. */ | |
/* package */ Class<T> mFragmentType; | |
/** Default constructor. required by android os. */ | |
@SuppressWarnings("unused") | |
public TActivity() { | |
} | |
/* package */ TActivity(@NonNull final Class<T> innerType) { | |
mFragmentType = innerType; | |
} | |
/** {@inheritDoc} */ | |
@Override | |
protected void onCreate(@Nullable final Bundle saved) { | |
super.onCreate(saved); | |
// this is dynamic view, so it does not recovered during activity re-creation. So we have always add it. | |
if (null == findViewById(ID)) { | |
// root view creation. | |
final FrameLayout view = new FrameLayout(this); | |
view.setId(ID); | |
setContentView(view, MATCH_PARENT); | |
} | |
// first time call, otherwise restore from saved instance state should work | |
if (null == saved) { | |
// fragment instance creation | |
final T fragment = instantiate(getIntent()); | |
if (null != mConfiguration) { | |
mConfiguration.call(fragment); | |
} | |
// integrate fragment | |
getSupportFragmentManager() | |
.beginTransaction() | |
.replace(ID, fragment, TAG_TEST_FRAGMENT) | |
.commit(); | |
} else { | |
mInstance = findFragment(); | |
} | |
resolveFragmentType(); | |
} | |
@SuppressWarnings("unchecked") | |
private void resolveFragmentType() { | |
if (null == mInstance) throw new AssertionError("Expected instance of the fragment."); | |
mFragmentType = (Class<T>) mInstance.getClass(); | |
} | |
/** Get instance of the Fragment from the fragment manager. Life instance. */ | |
@Nullable | |
@SuppressWarnings("unchecked") | |
public T findFragment() { | |
return (T) getSupportFragmentManager().findFragmentByTag(TAG_TEST_FRAGMENT); | |
} | |
/** Create a new instance of the Fragment based on provided intent. */ | |
@NonNull | |
/* package */ T instantiate(@NonNull final Intent intent) { | |
if (null != mInstance) { | |
return mInstance; | |
} | |
mInstance = mCreator.call(this, intent); | |
return mInstance; | |
} | |
/** Default re-creator of activity. */ | |
@NonNull | |
public Func0<ActivityController<TActivity<T>>> reCreator() { | |
return new Func0<ActivityController<TActivity<T>>>() { | |
@Override | |
public ActivityController<TActivity<T>> call() { | |
final Intent intent = getIntent(); | |
final Bundle saved = (null != intent) ? intent.getExtras() : null; | |
final ActivityController<TActivity<T>> result = testFragment(mFragmentType, saved); | |
result.get().setCreator(mCreator).setConfiguration(mConfiguration); | |
return result; | |
} | |
}; | |
} | |
//region Configuration | |
/** Assign instance created in some different way. */ | |
@NonNull | |
public TActivity<T> setInstance(@Nullable final T fragment) { | |
mInstance = fragment; | |
return this; | |
} | |
/** Assign instance creation implementation. */ | |
@NonNull | |
public TActivity<T> setCreator(@Nullable final Func2<Context, Intent, T> creator) { | |
mCreator = (null != creator) ? creator : new DoDefaultOs<T>(); | |
return this; | |
} | |
/** Assign instance configurator implementation. */ | |
@NonNull | |
public TActivity<T> setConfiguration(@Nullable final Action1<T> configuration) { | |
mConfiguration = configuration; | |
return this; | |
} | |
//endregion | |
} | |
/** Default way how OS create instance of the Fragment. */ | |
public static class DoDefaultOs<T> implements Func2<Context, Intent, T> { | |
@Override | |
@SuppressWarnings("unchecked") | |
public T call(final Context context, final Intent intent) { | |
final String className = intent.getComponent().getClassName(); | |
return (T) Fragment.instantiate(context, className, intent.getExtras()); | |
} | |
} | |
/** For fragment instance creation used 'public static T newInstance()' call. */ | |
public static class DoNewInstance<T> implements Func2<Context, Intent, T> { | |
private final Class<T> mClazz; | |
public DoNewInstance(@NonNull final Class<T> clazz) { | |
mClazz = clazz; | |
} | |
@TargetApi(Build.VERSION_CODES.KITKAT) | |
@Override | |
@SuppressWarnings("unchecked") | |
public T call(final Context context, final Intent intent) { | |
try { | |
final Method method = mClazz.getDeclaredMethod("newInstance"); | |
return (T) method.invoke(null); | |
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { | |
throw new RuntimeException("no newInstance() method", e); | |
} | |
} | |
} | |
/** Simplified construction of fragment instance. */ | |
@SuppressWarnings("unchecked") | |
public static class Help<T extends Fragment> { | |
public final DoNewInstance<T> creator; | |
public final ActivityController<TActivity<T>> controller; | |
public final TActivity<T> activity; | |
public final Func0<ActivityController<TActivity<T>>> reCreator; | |
private Help(@NonNull final Class<T> clazz) { | |
creator = new DoNewInstance<>(clazz); | |
controller = testFragment(clazz); | |
activity = controller.get().setCreator(creator); | |
reCreator = activity.reCreator(); | |
} | |
@NonNull | |
public static <T extends Fragment> Help<T> with(@NonNull final Class<T> clazz) { | |
return new Help<>(clazz); | |
} | |
public T setupFragment() { | |
controller.setup(); | |
return activity.findFragment(); | |
} | |
} | |
//endregion | |
} |
How to test Fragment full lifecycle.
@Test
public void testLifecycle() throws Exception {
RobolectricHelper.fullLifecycle(
Help.with(ParkingReceiptFragment.class),
new Action1<TActivity<ParkingReceiptFragment>>() {
@Override
public void call(final TActivity<ParkingReceiptFragment> activity) {
assertThat(activity.findFragment())
.describedAs("Expected fragment instance.")
.isNotNull();
assertThat(activity.findViewById(R.id.iv_close_icon))
.describedAs("Button not found. %s", RobolectricHelper.logViewHierarchy(activity))
.isNotNull();
assertThat(activity.findFragment().getParkingIdFromArgs())
.describedAs("Fragment should be created with fake parking ID: -1")
.isEqualTo(-1L);
}
});
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample: