Merge "Try to add some more documentation to testables"
This commit is contained in:
committed by
Android (Google) Code Review
commit
094f7ddfc0
@@ -52,7 +52,6 @@ public class TileServicesTest extends SysuiTestCase {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
TestableLooper.get(this).setAsMainLooper();
|
||||
mManagers = new ArrayList<>();
|
||||
QSTileHost host = new QSTileHost(mContext, null,
|
||||
mock(StatusBarIconController.class));
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* A runner with support for extra annotations provided by the Testables library.
|
||||
* @see UiThreadTest
|
||||
* @see TestableLooper.RunWithLooper
|
||||
*/
|
||||
public class AndroidTestingRunner extends BlockJUnit4ClassRunner {
|
||||
|
||||
|
||||
@@ -80,6 +80,10 @@ public abstract class BaseFragmentTest {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows tests to sub-class TestableContext if they want to provide any extended functionality
|
||||
* or provide a {@link LeakCheck} to the TestableContext upon instantiation.
|
||||
*/
|
||||
protected TestableContext getContext() {
|
||||
return new TestableContext(InstrumentationRegistry.getContext());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package android.testing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -28,6 +29,35 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility for dealing with the facts of Lifecycle. Creates trackers to check that for every
|
||||
* call to registerX, addX, bindX, a corresponding call to unregisterX, removeX, and unbindX
|
||||
* is performed. This should be applied to a test as a {@link org.junit.rules.TestRule}
|
||||
* and will only check for leaks on successful tests.
|
||||
* <p>
|
||||
* Example that will catch an allocation and fail:
|
||||
* <pre class="prettyprint">
|
||||
* public class LeakCheckTest {
|
||||
* @Rule public LeakCheck mLeakChecker = new LeakCheck();
|
||||
*
|
||||
* @Test
|
||||
* public void testLeak() {
|
||||
* Context context = new ContextWrapper(...) {
|
||||
* public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||
* mLeakChecker.getTracker("receivers").addAllocation(new Throwable());
|
||||
* }
|
||||
* public void unregisterReceiver(BroadcastReceiver receiver) {
|
||||
* mLeakChecker.getTracker("receivers").clearAllocations();
|
||||
* }
|
||||
* };
|
||||
* context.registerReceiver(...);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Note: {@link TestableContext} supports leak tracking when using
|
||||
* {@link TestableContext#TestableContext(Context, LeakCheck)}.
|
||||
*/
|
||||
public class LeakCheck extends TestWatcher {
|
||||
|
||||
private final Map<String, Tracker> mTrackers = new HashMap<>();
|
||||
@@ -40,6 +70,13 @@ public class LeakCheck extends TestWatcher {
|
||||
verify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a {@link Tracker}. Gets a tracker for the specified tag, creating one if necessary.
|
||||
* There should be one tracker for each pair of add/remove callbacks (e.g. one tracker for
|
||||
* registerReceiver/unregisterReceiver).
|
||||
*
|
||||
* @param tag Unique tag to use for this set of allocation tracking.
|
||||
*/
|
||||
public Tracker getTracker(String tag) {
|
||||
Tracker t = mTrackers.get(tag);
|
||||
if (t == null) {
|
||||
@@ -49,10 +86,13 @@ public class LeakCheck extends TestWatcher {
|
||||
return t;
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
private void verify() {
|
||||
mTrackers.values().forEach(Tracker::verify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds allocations associated with a specific callback (such as a BroadcastReceiver).
|
||||
*/
|
||||
public static class LeakInfo {
|
||||
private static final String TAG = "LeakInfo";
|
||||
private List<Throwable> mThrowables = new ArrayList<>();
|
||||
@@ -60,11 +100,20 @@ public class LeakCheck extends TestWatcher {
|
||||
LeakInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called once for each callback/listener added. addAllocation may be
|
||||
* called several times, but it only takes one clearAllocations call to remove all
|
||||
* of them.
|
||||
*/
|
||||
public void addAllocation(Throwable t) {
|
||||
// TODO: Drop off the first element in the stack trace here to have a cleaner stack.
|
||||
mThrowables.add(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the callback/listener has been removed. One call to
|
||||
* clearAllocations will counteract any number of calls to addAllocation.
|
||||
*/
|
||||
public void clearAllocations() {
|
||||
mThrowables.clear();
|
||||
}
|
||||
@@ -82,9 +131,16 @@ public class LeakCheck extends TestWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks allocations related to a specific tag or method(s).
|
||||
* @see #getTracker(String)
|
||||
*/
|
||||
public static class Tracker {
|
||||
private Map<Object, LeakInfo> mObjects = new ArrayMap<>();
|
||||
|
||||
private Tracker() {
|
||||
}
|
||||
|
||||
public LeakInfo getLeakInfo(Object object) {
|
||||
LeakInfo leakInfo = mObjects.get(object);
|
||||
if (leakInfo == null) {
|
||||
|
||||
@@ -27,7 +27,11 @@ import com.google.android.collect.Maps;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Alternative to a MockContentResolver that falls back to real providers.
|
||||
* A version of ContentResolver that allows easy mocking of providers.
|
||||
* By default it acts as a normal ContentResolver and returns all the
|
||||
* same providers.
|
||||
* @see #addProvider(String, ContentProvider)
|
||||
* @see #setFallbackToExisting(boolean)
|
||||
*/
|
||||
public class TestableContentResolver extends ContentResolver {
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.junit.runners.model.Statement;
|
||||
* <ul>
|
||||
* <li>System services can be mocked out with {@link #addMockSystemService}</li>
|
||||
* <li>Service binding can be mocked out with {@link #addMockService}</li>
|
||||
* <li>Resources can be mocked out using {@link #getOrCreateTestableResources()}</li>
|
||||
* <li>Settings support {@link TestableSettingsProvider}</li>
|
||||
* <li>Has support for {@link LeakCheck} for services and receivers</li>
|
||||
* </ul>
|
||||
@@ -50,10 +51,8 @@ import org.junit.runners.model.Statement;
|
||||
* <p>TestableContext should be defined as a rule on your test so it can clean up after itself.
|
||||
* Like the following:</p>
|
||||
* <pre class="prettyprint">
|
||||
* {@literal
|
||||
* @Rule
|
||||
* @Rule
|
||||
* private final TestableContext mContext = new TestableContext(InstrumentationRegister.getContext());
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public class TestableContext extends ContextWrapper implements TestRule {
|
||||
@@ -132,20 +131,26 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
: super.getResources();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getSystemService(String)
|
||||
*/
|
||||
public <T> void addMockSystemService(Class<T> service, T mock) {
|
||||
addMockSystemService(getSystemServiceName(service), mock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getSystemService(String)
|
||||
*/
|
||||
public void addMockSystemService(String name, Object service) {
|
||||
if (mMockSystemServices == null) mMockSystemServices = new ArrayMap<>();
|
||||
mMockSystemServices.put(name, service);
|
||||
}
|
||||
|
||||
public void addMockService(ComponentName component, IBinder service) {
|
||||
if (mMockServices == null) mMockServices = new ArrayMap<>();
|
||||
mMockServices.put(component, service);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a matching mock service has been added through {@link #addMockSystemService} then
|
||||
* that will be returned, otherwise the real service will be acquired from the base
|
||||
* context.
|
||||
*/
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (mMockSystemServices != null && mMockSystemServices.containsKey(name)) {
|
||||
@@ -166,6 +171,10 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
return mTestableContentResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will always return itself for a TestableContext to ensure the testable effects extend
|
||||
* to the application context.
|
||||
*/
|
||||
@Override
|
||||
public Context getApplicationContext() {
|
||||
// Return this so its always a TestableContext.
|
||||
@@ -199,6 +208,24 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
super.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mock service to be connected to by a bindService call.
|
||||
* <p>
|
||||
* Normally a TestableContext will pass through all bind requests to the base context
|
||||
* but when addMockService has been called for a ComponentName being bound, then
|
||||
* TestableContext will immediately trigger a {@link ServiceConnection#onServiceConnected}
|
||||
* with the specified service, and will call {@link ServiceConnection#onServiceDisconnected}
|
||||
* when the service is unbound.
|
||||
* </p>
|
||||
*/
|
||||
public void addMockService(ComponentName component, IBinder service) {
|
||||
if (mMockServices == null) mMockServices = new ArrayMap<>();
|
||||
mMockServices.put(component, service);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #addMockService(ComponentName, IBinder)
|
||||
*/
|
||||
@Override
|
||||
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
|
||||
if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable());
|
||||
@@ -206,6 +233,9 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
return super.bindService(service, conn, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #addMockService(ComponentName, IBinder)
|
||||
*/
|
||||
@Override
|
||||
public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
|
||||
Handler handler, UserHandle user) {
|
||||
@@ -214,6 +244,9 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
return super.bindServiceAsUser(service, conn, flags, handler, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #addMockService(ComponentName, IBinder)
|
||||
*/
|
||||
@Override
|
||||
public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
|
||||
UserHandle user) {
|
||||
@@ -232,6 +265,9 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #addMockService(ComponentName, IBinder)
|
||||
*/
|
||||
@Override
|
||||
public void unbindService(ServiceConnection conn) {
|
||||
if (mService != null) mService.getLeakInfo(conn).clearAllocations();
|
||||
@@ -243,6 +279,13 @@ public class TestableContext extends ContextWrapper implements TestRule {
|
||||
super.unbindService(conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the TestableContext has a mock binding for a specified component. Will return
|
||||
* true between {@link ServiceConnection#onServiceConnected} and
|
||||
* {@link ServiceConnection#onServiceDisconnected} callbacks for a mock service.
|
||||
*
|
||||
* @see #addMockService(ComponentName, IBinder)
|
||||
*/
|
||||
public boolean isBound(ComponentName component) {
|
||||
return mActiveServices != null && mActiveServices.containsValue(component);
|
||||
}
|
||||
|
||||
@@ -33,16 +33,15 @@ import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Creates a looper on the current thread with control over if/when messages are
|
||||
* executed. Warning: This class works through some reflection and may break/need
|
||||
* to be updated from time to time.
|
||||
* This is a wrapper around {@link TestLooperManager} to make it easier to manage
|
||||
* and provide an easy annotation for use with tests.
|
||||
*
|
||||
* @see TestableLooperTest TestableLooperTest for examples.
|
||||
*/
|
||||
public class TestableLooper {
|
||||
|
||||
private Looper mLooper;
|
||||
private MessageQueue mQueue;
|
||||
private boolean mMain;
|
||||
private Object mOriginalMain;
|
||||
private MessageHandler mMessageHandler;
|
||||
|
||||
private Handler mHandler;
|
||||
@@ -72,35 +71,20 @@ public class TestableLooper {
|
||||
mHandler = new Handler(mLooper);
|
||||
}
|
||||
|
||||
public void setAsMainLooper() throws NoSuchFieldException, IllegalAccessException {
|
||||
mMain = true;
|
||||
setAsMainInt();
|
||||
}
|
||||
|
||||
private void setAsMainInt() throws NoSuchFieldException, IllegalAccessException {
|
||||
Field field = mLooper.getClass().getDeclaredField("sMainLooper");
|
||||
field.setAccessible(true);
|
||||
if (mOriginalMain == null) {
|
||||
mOriginalMain = field.get(null);
|
||||
}
|
||||
field.set(null, mLooper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called if setAsMainLooper is called to restore the main looper when the
|
||||
* test is complete, otherwise the main looper will not be available for any subsequent
|
||||
* tests.
|
||||
* Must be called to release the looper when the test is complete, otherwise
|
||||
* the looper will not be available for any subsequent tests. This is
|
||||
* automatically handled for tests using {@link RunWithLooper}.
|
||||
*/
|
||||
public void destroy() throws NoSuchFieldException, IllegalAccessException {
|
||||
mQueueWrapper.release();
|
||||
if (mMain && mOriginalMain != null) {
|
||||
Field field = mLooper.getClass().getDeclaredField("sMainLooper");
|
||||
field.setAccessible(true);
|
||||
field.set(null, mOriginalMain);
|
||||
mOriginalMain = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback for all messages processed on this TestableLooper.
|
||||
*
|
||||
* @see {@link MessageHandler}
|
||||
*/
|
||||
public void setMessageHandler(MessageHandler handler) {
|
||||
mMessageHandler = handler;
|
||||
}
|
||||
@@ -119,6 +103,9 @@ public class TestableLooper {
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process messages in the queue until no more are found.
|
||||
*/
|
||||
public void processAllMessages() {
|
||||
while (processQueuedMessages() != 0) ;
|
||||
}
|
||||
@@ -183,6 +170,11 @@ public class TestableLooper {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotation that tells the {@link AndroidTestingRunner} to create a TestableLooper and
|
||||
* run this test/class on that thread. The {@link TestableLooper} can be acquired using
|
||||
* {@link #get(Object)}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface RunWithLooper {
|
||||
@@ -206,11 +198,15 @@ public class TestableLooper {
|
||||
|
||||
private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* For use with {@link RunWithLooper}, used to get the TestableLooper that was
|
||||
* automatically created for this test.
|
||||
*/
|
||||
public static TestableLooper get(Object test) {
|
||||
return sLoopers.get(test);
|
||||
}
|
||||
|
||||
public static class LooperFrameworkMethod extends FrameworkMethod {
|
||||
static class LooperFrameworkMethod extends FrameworkMethod {
|
||||
private HandlerThread mHandlerThread;
|
||||
|
||||
private final TestableLooper mTestableLooper;
|
||||
@@ -319,6 +315,11 @@ public class TestableLooper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to control the execution of messages on the looper, when set with
|
||||
* {@link #setMessageHandler(MessageHandler)} then {@link #onMessageHandled(Message)}
|
||||
* will get called back for every message processed on the {@link TestableLooper}.
|
||||
*/
|
||||
public interface MessageHandler {
|
||||
/**
|
||||
* Return true to have the message executed and delivered to target.
|
||||
|
||||
@@ -20,8 +20,8 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* When applied to a class, all tests, befores, and afters will behave as if
|
||||
* they have @UiThreadTest applied to them.
|
||||
* When applied to a class, all {@link org.junit.Test}s, {@link org.junit.After}s, and
|
||||
* {@link org.junit.Before} will behave as if they have @UiThreadTest applied to them.
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
@@ -21,8 +21,16 @@ import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
|
||||
/**
|
||||
* Utilities to make testing views easier.
|
||||
*/
|
||||
public class ViewUtils {
|
||||
|
||||
/**
|
||||
* Causes the view (and its children) to have {@link View#onAttachedToWindow()} called.
|
||||
*
|
||||
* This is currently done by adding the view to a window.
|
||||
*/
|
||||
public static void attachView(View view) {
|
||||
// Make sure hardware acceleration isn't turned on.
|
||||
view.getContext().getApplicationInfo().flags &=
|
||||
@@ -35,6 +43,11 @@ public class ViewUtils {
|
||||
.getSystemService(WindowManager.class).addView(view, lp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the view (and its children) to have {@link View#onDetachedFromWindow()} called.
|
||||
*
|
||||
* This is currently done by removing the view from a window.
|
||||
*/
|
||||
public static void detachView(View view) {
|
||||
InstrumentationRegistry.getContext()
|
||||
.getSystemService(WindowManager.class).removeViewImmediate(view);
|
||||
|
||||
Reference in New Issue
Block a user