如何更好地在Android上对Looper和Handler代码进行单元测试?

14
我使用android.os.Handler类在后台执行任务。进行单元测试时,我调用Looper.loop()来使测试线程等待后台任务线程完成其工作。稍后,我调用Looper.myLooper().quit()(也在测试线程中),以允许测试线程退出loop并恢复测试逻辑。
一切都很好,但当我想编写多个测试方法时就会出现问题。
问题在于Looper似乎没有设计成允许在同一线程上退出和重新启动,因此我被迫在单个测试方法内完成所有测试。
我查看了Looper的源代码,无法找到解决方法。
是否有其他测试Handler/Looper代码的方法?或者还有更友好的测试方式来编写后台任务类吗?

你能发一些关于这个的示例代码吗?我基本上有同样的问题,只是我还没有像你一样深入研究。 - user153275
4个回答

3

如果您没有使用AsyncTask,那么可以通过引入第二个looper线程(除了Android隐式为您创建的主线程)来轻松测试它。基本策略是使用CountDownLatch阻塞主线程,同时将所有回调委托给第二个looper线程。

这里的注意点是,您要测试的代码必须能够支持使用除默认主线程外的其他looper。我认为,无论如何都应该这样设计,以支持更强大和灵活的设计,而且也很容易实现。通常只需修改代码以接受可选的Looper参数,并使用它来构建您的Handler(如new Handler(myLooper))。对于AsyncTask,这种要求使得使用此方法进行测试成为不可能。我认为这是AsyncTask本身应解决的问题。

以下是一些示例代码:

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);
}

3

Looper的源代码显示,Looper.myLooper().quit()会将一个空消息加入消息队列中,告诉Looper它已经永远完成了消息处理。在那一点上,线程变成了死线程,我不知道有什么方法可以使其复活。当尝试向Handler发送消息时,您可能会看到类似“尝试向死线程发送消息”的错误消息。这就是它的含义。


0
受@Josh Guilfoyle答案的启发,我决定尝试使用反射来访问我所需的内容,以便制作自己的非阻塞和非退出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);
    }
}

现在在我的测试中(需要在UI线程上运行),我现在可以这样做:

@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() 
}

我不是说这是一个好主意,但到目前为止,对我来说已经起作用了。值得一试!我喜欢的是在某些hand.post(...)内部抛出的Exceptions将会破坏测试,否则情况并非如此。


0

我遇到了和你一样的问题。我也想为使用Handler的类编写一个测试用例。

与你所做的一样,我使用Looper.loop()来让测试线程开始处理队列中的消息。

为了停止它,我使用了MessageQueue.IdleHandler的实现来通知我当Looper被阻塞等待下一条消息时。当这种情况发生时,我调用quit()方法。但是,和你一样,当我创建多个测试用例时,我遇到了问题。

我想知道你是否已经解决了这个问题,并愿意与我(以及可能的其他人)分享:)

附注:我还想知道你如何调用Looper.myLooper().quit()

谢谢!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接