Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save minakov/892662b798261f33acb7488e0ea974d3 to your computer and use it in GitHub Desktop.
Save minakov/892662b798261f33acb7488e0ea974d3 to your computer and use it in GitHub Desktop.
Robolectric full lifecycle of activity/fragment looper.
package com.tsview.robolectric;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.widget.LinearLayout;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/** Test holder that simplify robolectric UI tests execution. */
@Config(sdk = Build.VERSION_CODES.LOLLIPOP, manifest = "src/main/AndroidManifest.xml")
@RunWith(RobolectricTestRunner.class)
public abstract class RobolectricTestsHelper {
/** New line delimiter. */
public final static String NEW_LINE = "\r\n";
/** Trace activity/fragment lifecycle states. Default: false. */
public static final String OPTION_LIFECYCLE = "trace-lifecycle";
/** Should log messages be with timestamp. By default: true; */
public final static String OPTION_TIMESTAMP = "use-timestamp";
/** Async Test synchronization instance. */
public final AtomicInteger SYNC_COUNTER = new AtomicInteger();
/** Dynamic options/configurations that influence on tests output. */
public final Map<String, Boolean> Options = new HashMap<>();
/** Standard Output Logger. Helps to save some useful results of tests as a part of execution. */
private final StringBuilder mLog = new StringBuilder(64 * 1024).append(NEW_LINE);
/**
* 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 <T extends Activity> ActivityController<T> fullLifecycle(
@NonNull ActivityController<T> controller,
@Nullable final Runnable onRestart,
@Nullable final Runnable onResume,
@Nullable final Runnable onVisible,
@Nullable final RecreateRunnable<T> onRecreate) {
Bundle savedInstanceState = null;
final boolean doTrace = option(OPTION_LIFECYCLE, false);
// do recreate only if defined callback
int recreateLoops = (null != onRecreate) ? 1 : 0;
do {
if (doTrace) trace("state - onCreate : " + recreateLoops);
controller.create(savedInstanceState);
// CYCLE #1: emulate activity restart
int lifeLoops = 1;
do {
if (doTrace) trace("state - onStart : " + lifeLoops);
controller.start();
if (null != savedInstanceState) {
controller.restoreInstanceState(savedInstanceState);
controller.postCreate(savedInstanceState);
}
// CYCLE #1.1: emulate show/hide
int loops = 1;
do {
if (doTrace) trace("state - onResume : " + loops);
if (null != onResume) onResume.run();
controller.resume(); // --> onPostResume()
// TODO: onAttachedToWindow()
controller.visible(); // --> onCreateOptionsMenu(), onUserInteraction()
if (null != onVisible) onVisible.run();
controller.userLeaving();
controller.pause();
if (doTrace) trace("state - onPause");
loops--;
} while (loops >= 0);
// CHECK-ME: robolectric call it before #pause()
controller.saveInstanceState(savedInstanceState = new Bundle());
controller.stop();
if (doTrace) trace("state - onStop");
// TODO: onRetainNonConfigurationInstance() --> controller.get().onRetainNonConfigurationInstance();
// go-to onRestart() state
if (lifeLoops > 0) {
if (null != onRestart) onRestart.run();
// during restart we do not need the savedInstanceState, drop the instance
savedInstanceState = null;
if (doTrace) trace("state - onRestart");
controller.restart();
}
lifeLoops--;
} while (lifeLoops >= 0);
if (doTrace) trace("state - onDestroy");
controller.destroy();
// save instance and recover it for additional lifecycle loop
if (recreateLoops > 0) {
if (null != onRecreate)
controller = onRecreate.recreate(controller);
if (doTrace) trace("state - recreate : " + recreateLoops);
controller.attach();
}
recreateLoops--;
} while (recreateLoops >= 0);
return controller; // can be a new instance due to re-create execution
}
//region --> Fragment Lifecycle emulation
public static <T extends Fragment> ActivityController<FragmentHostingActivity> inject(
@NonNull ActivityController<FragmentHostingActivity> controller,
@Nullable Configure<T> configuration) {
controller.get().setConfiguration(configuration);
return controller;
}
public static <T extends Fragment> ActivityController<FragmentHostingActivity> testFragment(final Class<T> clazz) {
return testFragment(clazz, null);
}
public static <T extends Fragment> ActivityController<FragmentHostingActivity> 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);
return testActivity(FragmentHostingActivity.class, intent);
}
//endregion
//region Options
/** Check the option. */
public boolean option(final String name, final boolean $default) {
if (Options.containsKey(name)) {
return Options.get(name);
}
return $default;
}
/** Set option to a new value. */
public void setOption(final String name, final boolean value) {
Options.put(name, value);
}
//endregion
//region Standard Output
/** Get access to the logs memory storage directly. */
@NonNull
protected StringBuilder getRawLogger() {
return mLog;
}
public void log(final Level level, final String tag, final String msg) {
final long last = option(OPTION_THREAD_TIMESTAMP, true) ? mLastMsg.get() : mTimestamp;
final String timestamp = String.format(Locale.US, " (+%-5.3f ms)", millisFrom(last));
final boolean doTimestamp = option(OPTION_TIMESTAMP, true);
mLog.append(level.toString().charAt(0)).append(" : ")
.append(tag).append(" : ")
.append(msg)
.append(doTimestamp ? timestamp : "")
.append(NEW_LINE);
// update timestamps
mLastMsg.set(mTimestamp = System.nanoTime());
}
/** trace single message without formatting. */
public void trace(final String msg) {
log(Level.INFO, "--", msg);
}
/** trace with formatting. */
public void trace(final String format, final Object... args) {
final String message = (null == args) ? format : String.format(Locale.US, format, args);
log(Level.INFO, "--", message);
}
/**
* Convert nanoseconds to milliseconds with high accuracy.
*
* @param nanos nanoseconds to convert.
* @return total milliseconds.
*/
public static double toMillis(final long nanos) {
return nanos / 1000.0 /* micros in 1 milli */ / 1000.0 /* nanos in 1 micro */;
}
/** Get duration in milliseconds from start point till now. */
public static double millisFrom(final long start) {
return toMillis(System.nanoTime() - start);
}
//endregion
//region --> Nested Declarations
/** Implement interface for making possible testing of the activity re-creation. */
public interface RecreateRunnable<T extends Activity> {
ActivityController<T> recreate(final ActivityController<T> old);
}
/** Configure instance of the fragment before it injecting into activity. */
public interface Configure<T extends Fragment> {
void configure(final T instance);
}
/** Hosting activity that can be used for any fragment testing. */
public static class FragmentHostingActivity extends FragmentActivity {
/** Tag for easier finding of the fragment. */
public static final String TAG_TEST_FRAGMENT = "TAG_TEST_FRAGMENT";
/** Cache of the instance. */
private Fragment mInstance;
private Configure<Fragment> mConfiguration;
@Override
protected void onCreate(@Nullable final Bundle saved) {
super.onCreate(saved);
// first time call, otherwise restore from saved instance state should work
if (null == saved) {
// root view creation
final LinearLayout view = new LinearLayout(this);
view.setId(R.id.tag_view_holder);
setContentView(view);
// fragment instance creation
final Fragment fragment = getFragmentInstantiate(getIntent());
if (null != mConfiguration) {
mConfiguration.configure(fragment);
}
// integrate fragment
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.tag_view_holder, fragment, TAG_TEST_FRAGMENT)
.commit();
}
}
/** Get instance of the Fragment from the fragment manager. Life instance. */
@Nullable
public Fragment getTestFragment() {
return getSupportFragmentManager().findFragmentByTag(TAG_TEST_FRAGMENT);
}
/** Create a new instance of the Fragment based on provided intent. */
public Fragment getFragmentInstantiate(@NonNull final Intent intent) {
if (null != mInstance)
return mInstance;
final String className = intent.getComponent().getClassName();
mInstance = Fragment.instantiate(this, className, intent.getExtras());
return mInstance;
}
public void setFragmentInstantiate(final Fragment fragment) {
mInstance = fragment;
}
public <T extends Fragment> FragmentHostingActivity setConfiguration(final Configure<T> configuration) {
mConfiguration = (Configure<Fragment>) ((Configure<?>) configuration);
return this;
}
}
//endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment