How to better unit test Looper and Handler code on Android?
Asked Answered
H

4

14

I use the android.os.Handler class to perform tasks on the background. When unit testing these, I call Looper.loop() to make the test thread wait for the background task thread to do its thing. Later, I call Looper.myLooper().quit() (also in the test thread), to allow the test thread to quit the loop and resume the testing logic.

It's all fine and dandy until I want to write more than one test method.

The problem is that Looper doesn't seem to be designed to allow quitting and restarting on the same thread, so I am forced to do all of my testing inside a single test method.

I looked into the source code of Looper, and couldn't find a way around it.

Is there any other way to test my Hander/Looper code? Or maybe some more test friendly way to write my background task class?

Hellenistic answered 7/9, 2010 at 5:23 Comment(1)
Can you post some sample code for this? I have basically the same question, except I haven't gotten as far as you.Iniquity
V
3

The source code for Looper reveals that Looper.myLooper().quit() enqueues a null message in the Message queue, which tells Looper that it is done processing messages FOREVER. Essentially, the thread becomes a dead thread at that point, and there is no way to revive it that I know of. You may have seen error messages when attempting to post messages to the Handler after quit() is called to the effect "attempting to send message to dead thread". That is what that means.

Vendetta answered 1/7, 2011 at 0:54 Comment(0)
R
3

This can actually be tested easily if you aren't using AsyncTask by introducing a second looper thread (other than the main one created for you implicitly by Android). The basic strategy then is to block the main looper thread using a CountDownLatch while delegating all your callbacks to the second looper thread.

The caveat here is that your code under test must be able to support using a looper other than the default main one. I would argue that this should be the case regardless to support a more robust and flexible design, and it is also fortunately very easy. In general, all that must be done is to modify your code to accept an optional Looper parameter and use that to construct your Handler (as new Handler(myLooper)). For AsyncTask, this requirement makes it impossible to test it with this approach. A problem that I think should be remedied with AsyncTask itself.

Some sample code to get you started:

public void testThreadedDesign() {
    final CountDownLatch latch = new CountDownLatch(1);

    /* Just some class to store your result. */
    final TestResult result = new TestResult(); 

    HandlerThread testThread = new HandlerThread("testThreadedDesign thread");
    testThread.start();

    /* This begins a background task, say, doing some intensive I/O.
     * The listener methods are called back when the job completes or
     * fails. */
    new ThingThatOperatesInTheBackground().doYourWorst(testThread.getLooper(),
            new SomeListenerThatTotallyShouldExist() {
        public void onComplete() {
            result.success = true;
            finished();
        }

        public void onFizzBarError() {
            result.success = false;
            finished();
        }

        private void finished() {
            latch.countDown();
        }
    });

    latch.await();

    testThread.getLooper().quit();

    assertTrue(result.success);
}
Rambutan answered 12/12, 2012 at 2:43 Comment(0)
S
0

I've stumbled in the same issue as yours. I also wanted to make a test case for a class that use a Handler.

Same as what you did, I use the Looper.loop() to have the test thread starts handling the queued messages in the handler.

To stop it, I used the implementation of MessageQueue.IdleHandler to notify me when the looper is blocking to wait the next message to come. When it happen, I call the quit() method. But again, same as you I got a problem when I make more than one test case.

I wonder if you already solved this problem and perhaps care to share it with me (and possibly others) :)

PS: I also would like to know how you call your Looper.myLooper().quit().

Thanks!

Shutout answered 9/12, 2010 at 9:33 Comment(0)
S
0

Inspired by @Josh Guilfoyle's answer, I decided to try to use reflection to get access to what I needed in order to make my own non-blocking and non-quitting Looper.loop().

/**
 * Using reflection, steal non-visible "message.next"
 * @param message
 * @return
 * @throws Exception
 */
private Message _next(Message message) throws Exception {
    Field f = Message.class.getDeclaredField("next");
    f.setAccessible(true);
    return (Message)f.get(message);
}

/**
 * Get and remove next message in local thread-pool. Thread must be associated with a Looper.
 * @return next Message, or 'null' if no messages available in queue.
 * @throws Exception
 */
private Message _pullNextMessage() throws Exception {
    final Field _messages = MessageQueue.class.getDeclaredField("mMessages");
    final Method _next = MessageQueue.class.getDeclaredMethod("next");

    _messages.setAccessible(true);
    _next.setAccessible(true);

    final Message root = (Message)_messages.get(Looper.myQueue());
    final boolean wouldBlock = (_next(root) == null);
    if(wouldBlock)
        return null;
    else
        return (Message)_next.invoke(Looper.myQueue());
}

/**
 * Process all pending Messages (Handler.post (...)).
 * 
 * A very simplified version of Looper.loop() except it won't
 * block (returns if no messages available). 
 * @throws Exception
 */
private void _doMessageQueue() throws Exception {
    Message msg;
    while((msg = _pullNextMessage()) != null) {
        msg.getTarget().dispatchMessage(msg);
    }
}

Now in my tests (which need to run on the UI thread), I can now do:

@UiThreadTest
public void testCallbacks() throws Throwable {
    adapter = new UpnpDeviceArrayAdapter(getInstrumentation().getContext(), upnpService);

    assertEquals(0, adapter.getCount());

    upnpService.getRegistry().addDevice(createRemoteDevice());
    // the adapter posts a Runnable which adds the new device.
    // it has to because it must be run on the UI thread. So we
    // so we need to process this (and all other) handlers before
    // checking up on the adapter again.
    _doMessageQueue();

    assertEquals(2, adapter.getCount());

    // remove device, _doMessageQueue() 
}

I'm not saying this is a good idea, but so far it's been working for me. Might be worth trying out! What I like about this is that Exceptions that are thrown inside some hander.post(...) will break the tests, which is not the case otherwise.

Swob answered 21/1, 2013 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.