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

Posted

技术标签:

【中文标题】如何在 Android 上更好地对 Looper 和 Handler 代码进行单元测试?【英文标题】:How to better unit test Looper and Handler code on Android? 【发布时间】:2011-04-09 00:45:42 【问题描述】:

我使用 android.os.Handler 类在后台执行任务。当对这些进行单元测试时,我调用Looper.loop() 使测试线程等待后台任务线程执行其操作。后来我调用Looper.myLooper().quit()(也在测试线程中),让测试线程退出loop,恢复测试逻辑。

在我想编写多个测试方法之前,一切都很好。

问题在于 Looper 似乎没有被设计为允许在同一个线程上退出和重新启动,所以我不得不在一个测试方法中完成所有测试。

我查看了 Looper 的源代码,但找不到解决方法。

还有其他方法可以测试我的 Hander/Looper 代码吗?或者也许是一些对测试更友好的方式来编写我的后台任务类?

【问题讨论】:

您可以为此发布一些示例代码吗?我有基本相同的问题,只是我没有达到你的程度。 【参考方案1】:

我偶然发现了和你一样的问题。我还想为使用Handler 的类制作一个测试用例。

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

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

我想知道你是否已经解决了这个问题,也许愿意与我(可能还有其他人)分享它:)

PS:我也想知道你怎么称呼你的Looper.myLooper().quit()

谢谢!

【讨论】:

【参考方案2】:

Looper 的源代码显示 Looper.myLooper().quit() 在消息队列中加入了一个空消息,这告诉 Looper 它已经完成了对消息的永远处理。从本质上讲,该线程在那时变成了死线程,并且据我所知,没有办法恢复它。尝试将消息发布到 调用 quit() 之后的处理程序以达到“尝试向死线程发送消息”的效果。就是这个意思。

【讨论】:

【参考方案3】:

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

这里需要注意的是,您的被测代码必须能够支持使用除默认 main 之外的 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);

【讨论】:

【参考方案4】:

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

我并不是说这是一个好主意,但到目前为止它一直对我有用。可能值得一试!我喜欢这一点的是,在一些 hander.post(...) 中抛出的 Exceptions 会破坏测试,否则情况并非如此。

【讨论】:

以上是关于如何在 Android 上更好地对 Looper 和 Handler 代码进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

Android 如何在jni层使用Looper

Android异步消息机制

Android App运行核心,Handler,Looper,Message

Android App运行核心,Handler,Looper,Message

Android App运行核心,Handler,Looper,Message

在 Android 中使用 Looper.prepare() 的细节