如何在 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 App运行核心,Handler,Looper,Message
Android App运行核心,Handler,Looper,Message