Skip to content

Instantly share code, notes, and snippets.

@nhachicha
Last active November 10, 2015 16:56
Show Gist options
  • Save nhachicha/e993fe9b5f09f0595ccd to your computer and use it in GitHub Desktop.
Save nhachicha/e993fe9b5f09f0595ccd to your computer and use it in GitHub Desktop.
Example demonstrating problem you may encounter using the Looper/Handler with unit tests. & a pattern at the end to avoid them
public void testLooperNotProcessingMessageTestWillNotExit () throws InterruptedException {
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
}
};
boolean messageSent = handler.sendEmptyMessage(42);
assertTrue(messageSent);
}
public void testLooperUnreachableCode () throws InterruptedException {
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
}
};
boolean messageSent = handler.sendEmptyMessage(42);
Looper.loop();
// !!! Unreachable code below !!!
assertTrue(messageSent);
}
public void testLooperQuittingLooper () throws InterruptedException {
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
Looper.myLooper().quit();
}
};
boolean messageSent = handler.sendEmptyMessage(42);
Looper.loop();
// Looper.prepare(); you can't call/reuse the Looper again
// in this test You'll get a RuntimeException:
// Only one Looper may be created per thread
assertTrue(messageSent);
}
public void testUsingHandlerThread () throws Throwable {
final CountDownLatch signalTestCompleted = new CountDownLatch(1);
final HandlerThread backgroundThread = new HandlerThread("bg");
backgroundThread.start(); // start the looper
Handler handler = new Handler(backgroundThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
assertEquals("bg", Thread.currentThread().getName());
assertEquals(42, msg.what);
signalTestCompleted.countDown();
}
};
handler.sendEmptyMessage(42);
signalTestCompleted.await();
backgroundThread.quit();
}
public void testUsingLooperWithIdleHandler() throws Throwable {
final CountDownLatch signalTestCompleted = new CountDownLatch(1);
final Throwable[] threadAssertionError = new Throwable[1];
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// prepare a Looper for this Thread
Looper.prepare();
// register IdleHandler to quit the Looper once all messages have proceeded
// Let the first queueIdle invocation pass, because it occurs before the first message is received.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// this is the last message in our Queue, we can test
// any logic we want, we chose simply to complete our test in our case
signalTestCompleted.countDown();
return false; // unregister from the future IdleHandler events
}
});
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
}
};
handler.sendEmptyMessage(42);
Looper.loop();
} catch (Throwable e) {
threadAssertionError[0] = e; // important to catch exception & AssertionFailedError
}
}
});
// make the InstrumentationThread wait until our test complete
signalTestCompleted.await();
if (null != threadAssertionError[0]) {
// throw any assertion errors happened in the background thread
throw threadAssertionError[0];
}
executorService.shutdownNow();
}
// Pattern that gives you full control over the Looper, with no side effect on other test
// + guarantee to run any finally block
public void testLooperRecommendedPatternFinallyGotcha () throws Throwable {
final CountDownLatch signalTestCompleted = new CountDownLatch(1);
final Throwable[] threadAssertionError = new Throwable[1];
final Looper[] testLooper = new Looper[1];
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
InputStream is = null; // expensive resource
try {
// prepare a Looper for this Thread
Looper.prepare();
testLooper[0] = Looper.myLooper();
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
signalTestCompleted.countDown();
}
};
handler.sendEmptyMessage(42);
Looper.loop();
} catch (Throwable e) {
threadAssertionError[0] = e; // important to catch exception & AssertionFailedError
} finally {
// this will not run (even if we shutdown the ExecutorService)
// unless the Looper is stopped before
if (is != null) {
try {is.close();} catch (IOException e) {}
}
}
}
}
);
// make the InstrumentationThread wait until our test complete
signalTestCompleted.await();
// throw any assertion errors happened in the background thread
if(threadAssertionError[0] != null) {
throw threadAssertionError[0];
}
// before shutting down the executor stop the Looper to give
// the finally block a chance to run
if (testLooper[0] != null) {
testLooper[0].quit(); // or quitSafely for API level >= 18 terminate as soon as all remaining messages
// in the message queue that are already due to be delivered have been handled.
}
executorService.shutdownNow();
}
public void testUsingLooperRecommendedPattern () throws Throwable {
final CountDownLatch signalTestCompleted = new CountDownLatch(1);
final Throwable[] threadAssertionError = new Throwable[1];
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// prepare a Looper for this Thread
Looper.prepare();
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
assertEquals(42, msg.what);
signalTestCompleted.countDown();
}
};
handler.sendEmptyMessage(42);
Looper.loop();
} catch (Throwable e) {
threadAssertionError[0] = e; // important to catch exception & AssertionFailedError
}
}
});
// make the InstrumentationThread wait until our test complete
signalTestCompleted.await();
if (null != threadAssertionError[0]) {
// throw any assertion errors happened in the background thread
throw threadAssertionError[0];
}
executorService.shutdownNow();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment