14

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

在我想编写不止一种测试方法之前,一切都很好。

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

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

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

4

4 回答 4

3

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

于 2011-07-01T00:54:08.047 回答
3

如果您没有AsyncTask通过引入第二个活套线程(Android 为您隐式创建的主线程除外)不使用,这实际上可以很容易地进行测试。然后,基本策略是使用CountDownLatchwhile 将所有回调委托给第二个 Looper 线程来阻塞主 Looper 线程。

这里需要注意的是,您的被测代码必须能够支持使用除默认 main 之外的 looper。我认为无论如何支持更健壮和灵活的设计都应该是这种情况,而且幸运的是它也很容易。一般来说,必须做的就是修改您的代码以接受可选Looper参数并使用它来构造您的Handler(as 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);
}
于 2012-12-12T02:43:04.457 回答
0

我偶然发现了和你一样的问题。我还想为使用Handler.

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

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

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

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

谢谢!

于 2010-12-09T09:33:24.210 回答
0

受@Josh Guilfoyle 的回答启发,我决定尝试使用反射来访问我需要的东西,以使我自己的 non-blocking 和 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);
    }
}

现在在我的测试中(需要在 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() 
}

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

于 2013-01-21T12:49:18.480 回答