Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,32 @@ The options to enable some features on testing are same as [adb instrument](http
- coverage
- coverageFile

If you need to run tests from Android Studio, please use [Android Tests](https://www.jetbrains.com/help/idea/2016.1/run-debug-configuration-android-test.html) Configuration.
If you need to run tests from Android Studio, please use [Android Tests](https://www.jetbrains.com/help/idea/2016.1/run-debug-configuration-android-test.html) Configuration. Running the following Gradle tasks from Android Studio will typically work with these tests, where YourBuildName is your build name in camel-case:

- :build
- :installYourBuildNameAndroidTest
- :connectedYourBuildNameAndroidTest

Compatability with Android UI testing frameworks
------------------------------------------------

This framework has been tested to be fully compatable with the UIAutomator framework; just make sure to import the AndroidTestNGSupport class' getInstrumentation() and getContext() methods. It's also compatable with the Espresso framework. Make sure to set up your Espresso tests with methods such as the following (this is just the code I got to work; it may be suboptimal!):

@Inject
public YourTestNameHere(Instrumentation instrumentation) {
this.mInstrumentation = instrumentation;
}

@BeforeTest
public void setUp() {
InstrumentationRegistry.registerInstance(mInstrumentation,
new Bundle());
Intent intent = new Intent(getContext(), YourAppActivityNameHere.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity = YourAppActivityNameHere.class.cast(mInstrumentation.startActivitySync(intent));
mInstrumentation.waitForIdleSync();
}


License
-------
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
exclude group: 'aopalliance', module: 'aopalliance'
}

// Support annotations required for ActivityLifeCycleMonitor
compile('com.android.support.test:runner:0.5')

androidTestCompile('com.google.inject:guice:3.0:no_aop') {
exclude group: 'org.sonatype.sisu.inject', module: 'cglib'
exclude group: 'aopalliance', module: 'aopalliance'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package de.lemona.android.testng;
/*
* This class is appropriated from the Android Open Source Project's JUnitTestRunner
*
* Their license is as follows:
*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import static android.support.test.internal.util.Checks.checkNotNull;
import android.app.Activity;
import android.os.Looper;
import android.support.test.runner.lifecycle.ActivityLifecycleCallback;
import android.support.test.runner.lifecycle.ActivityLifecycleMonitor;
import android.support.test.runner.lifecycle.Stage;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public final class ActivityLifecycleMonitorImpl implements ActivityLifecycleMonitor {

private static final String TAG = "LifecycleMonitor";
private final boolean mDeclawThreadCheck;
public ActivityLifecycleMonitorImpl() {
this(false);
}
// For Testing
public ActivityLifecycleMonitorImpl(boolean declawThreadCheck) {
this.mDeclawThreadCheck = declawThreadCheck;
}
// Accessed from any thread.
private List<WeakReference<ActivityLifecycleCallback>> mCallbacks =
new ArrayList<WeakReference<ActivityLifecycleCallback>>();
// Only accessed on main thread.
private List<ActivityStatus> mActivityStatuses = new ArrayList<ActivityStatus>();
@Override
public void addLifecycleCallback(ActivityLifecycleCallback callback) {
// there will never be too many callbacks, so iterating over a list will probably
// be faster then the constant time costs of setting up and maintaining a map.
checkNotNull(callback);
synchronized (mCallbacks) {
boolean needsAdd = true;
Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
while (refIter.hasNext()) {
ActivityLifecycleCallback storedCallback = refIter.next().get();
if (null == storedCallback) {
refIter.remove();
} else if (storedCallback == callback) {
needsAdd = false;
}
}
if (needsAdd) {
mCallbacks.add(new WeakReference<ActivityLifecycleCallback>(callback));
}
}
}
@Override
public void removeLifecycleCallback(ActivityLifecycleCallback callback) {
checkNotNull(callback);
synchronized (mCallbacks) {
Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
while (refIter.hasNext()) {
ActivityLifecycleCallback storedCallback = refIter.next().get();
if (null == storedCallback) {
refIter.remove();
} else if (storedCallback == callback) {
refIter.remove();
}
}
}
}
@Override
public Stage getLifecycleStageOf(Activity activity) {
checkMainThread();
checkNotNull(activity);
Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
while (statusIterator.hasNext()) {
ActivityStatus status = statusIterator.next();
Activity statusActivity = status.mActivityRef.get();
if (null == statusActivity) {
statusIterator.remove();
} else if (activity == statusActivity) {
return status.mLifecycleStage;
}
}
throw new IllegalArgumentException("Unknown activity: " + activity);
}
@Override
public Collection<Activity> getActivitiesInStage(Stage stage) {
checkMainThread();
checkNotNull(stage);
List<Activity> activities = new ArrayList<Activity>();
Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
while (statusIterator.hasNext()) {
ActivityStatus status = statusIterator.next();
Activity statusActivity = status.mActivityRef.get();
if (null == statusActivity) {
statusIterator.remove();
} else if (stage == status.mLifecycleStage) {
activities.add(statusActivity);
}
}
return activities;
}
/**
* Called by the runner after a particular onXXX lifecycle method has been called on a given
* activity.
*/
public void signalLifecycleChange(Stage stage, Activity activity) {
// there are never too many activities in existence in an application - so we keep
// track of everything in a single list.
Log.d(TAG, "Lifecycle status change: " + activity + " in: " + stage);
boolean needsAdd = true;
Iterator<ActivityStatus> statusIterator = mActivityStatuses.iterator();
while (statusIterator.hasNext()) {
ActivityStatus status = statusIterator.next();
Activity statusActivity = status.mActivityRef.get();
if (null == statusActivity) {
statusIterator.remove();
} else if (activity == statusActivity) {
needsAdd = false;
status.mLifecycleStage = stage;
}
}
if (needsAdd) {
mActivityStatuses.add(new ActivityStatus(activity, stage));
}
synchronized (mCallbacks) {
Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator();
while (refIter.hasNext()) {
ActivityLifecycleCallback callback = refIter.next().get();
if (null == callback) {
refIter.remove();
} else {
try {
Log.d(TAG, "running callback: " + callback);
callback.onActivityLifecycleChanged(activity, stage);
Log.d(TAG, "callback completes: " + callback);
} catch (RuntimeException re) {
Log.e(TAG, String.format(
"Callback threw exception! (callback: %s activity: %s stage: %s)",
callback,
activity,
stage),
re);
}
}
}
}
}
private void checkMainThread() {
if (mDeclawThreadCheck) {
return;
}
if (!Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
throw new IllegalStateException(
"Querying activity state off main thread is not allowed.");
}
}
private static class ActivityStatus {
private final WeakReference<Activity> mActivityRef;
private Stage mLifecycleStage;
ActivityStatus(Activity activity, Stage stage) {
this.mActivityRef = new WeakReference<Activity>(checkNotNull(activity));
this.mLifecycleStage = checkNotNull(stage);
}
}
}
83 changes: 83 additions & 0 deletions src/main/java/de/lemona/android/testng/TestNGRunner.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package de.lemona.android.testng;

import android.app.Instrumentation;
import android.app.Activity;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import android.support.test.runner.lifecycle.Stage;
import android.os.Bundle;
import android.os.Debug;
import android.util.Log;
Expand All @@ -16,6 +20,8 @@
import java.io.InputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.ArrayList;
import java.util.EnumSet;

import dalvik.system.DexFile;

Expand All @@ -27,11 +33,15 @@
public class TestNGRunner extends Instrumentation {

private String targetPackage = null;
private ActivityLifecycleMonitorImpl mLifecycleMonitor = new ActivityLifecycleMonitorImpl();
private TestNGArgs args;

@Override
public void onCreate(Bundle arguments) {
super.onCreate(arguments);
InstrumentationRegistry.registerInstance(this, arguments);
ActivityLifecycleMonitorRegistry.registerInstance(mLifecycleMonitor);

args = parseRunnerArgument(arguments);
targetPackage = this.getTargetContext().getPackageName();
this.start();
Expand Down Expand Up @@ -138,6 +148,13 @@ public void onStart() {
}
}

@Override
public void finish(int resultCode, Bundle results) {
finishActivities();
ActivityLifecycleMonitorRegistry.registerInstance(null);
super.finish(resultCode, results);
}

private void setupDexmakerClassloader() {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
// must set the context classloader for apps that use a shared uid, see
Expand All @@ -147,4 +164,70 @@ private void setupDexmakerClassloader() {
// newClassLoader.toString(), originalClassLoader.toString()));
Thread.currentThread().setContextClassLoader(newClassLoader);
}
@Override
public void callActivityOnDestroy(Activity activity) {
super.callActivityOnDestroy(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.DESTROYED, activity);
}

@Override
public void callActivityOnRestart(Activity activity) {
super.callActivityOnRestart(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.RESTARTED, activity);
}

@Override
public void callActivityOnCreate(Activity activity, Bundle bundle) {
mLifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity);
super.callActivityOnCreate(activity, bundle);
mLifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity);
}

@Override
public void callActivityOnStart(Activity activity) {
try {
super.callActivityOnStart(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.STARTED, activity);
} catch (RuntimeException re) {
throw re;
}
}

@Override
public void callActivityOnStop(Activity activity) {
super.callActivityOnStop(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.STOPPED, activity);
}

@Override
public void callActivityOnResume(Activity activity) {
super.callActivityOnResume(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.RESUMED, activity);
}

@Override
public void callActivityOnPause(Activity activity) {
super.callActivityOnPause(activity);
mLifecycleMonitor.signalLifecycleChange(Stage.PAUSED, activity);
}

private void finishActivities() {
List<Activity> activities = new ArrayList<Activity>();

for (Stage s : EnumSet.range(Stage.CREATED, Stage.PAUSED)) {
activities.addAll(mLifecycleMonitor.getActivitiesInStage(s));
}

Log.i(TAG, "Activities that are still in CREATED to PAUSED: " + activities.size());
for (Activity activity : activities) {
if (!activity.isFinishing()) {
try {
Log.i(TAG, "Stopping activity: " + activity);
activity.finish();
} catch (RuntimeException e) {
Log.e(TAG, "Failed to stop activity.", e);
}
}
}
}
}