Forked from OleksandrKucherenko/RobolectricTestsHelper.java
Created
July 11, 2016 05:53
-
-
Save minakov/892662b798261f33acb7488e0ea974d3 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 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