Skip to content

Instantly share code, notes, and snippets.

@OleksandrKucherenko
Last active April 27, 2020 09:30
Show Gist options
  • Save OleksandrKucherenko/90ed909a2a1b0411b6c6de8798f5a7fe to your computer and use it in GitHub Desktop.
Save OleksandrKucherenko/90ed909a2a1b0411b6c6de8798f5a7fe to your computer and use it in GitHub Desktop.
Robolectric full lifecycle of activity/fragment looper.
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&lt;TActivity&lt;Login>> controller = RobolectricHelper.testFragment(Login.class);
* TActivity&lt;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&lt;TActivity&lt;Login>> controller = RobolectricHelper.testFragment(Login.class);
* TActivity&lt;Login> activity = controller.get();
* Action1&lt;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
}
@OleksandrKucherenko
Copy link
Author

OleksandrKucherenko commented Jul 1, 2016

Sample:

    final ActivityController<FragmentHostingActivity> controller = testFragment(CallAuthLoginFragment.class);
    final FragmentHostingActivity activity = controller.get();

    // assign instance of fragment to the Activity for testing ()
    activity.setFragmentInstantiate((Fragment) screen);

    // run full life cycle for fragment instance
    fullLifecycle(controller, null, null, new Runnable() {
      @Override
      public void run() {
        final View dynamicTop = activity.findViewById(R.id.top_content);
        final View dynamicBottom = activity.findViewById(R.id.bottom_content);

        assertThat(dynamicTop, notNullValue());
        assertThat(dynamicBottom, notNullValue());
        assertThat(UI.text(activity, R.id.rtv_title).second.toString(), is(messages.getTitle(ILocalized.CALL_AUTH_PAGE_TITLE)));
        assertThat(UI.text(activity, R.id.rtv_description_with_number).second.toString(), is(messages.getTitle(ILocalized.CALL_AUTH_PAGE_DESCRIPTION_WITH_NUMBER)));
        assertThat(UI.text(activity, R.id.rtv_description).second.toString(), is(messages.getTitle(ILocalized.CALL_AUTH_PAGE_DESCRIPTION)));
      }
    }, null);

@OleksandrKucherenko
Copy link
Author

OleksandrKucherenko commented Sep 5, 2017

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