Как лучше unit test Код Looper и Handler на Android?
Я использую класс android.os.Handler для выполнения задач на фоне. Когда модуль тестирует их, я вызываю Looper.loop()
, чтобы тестовый поток ожидал, когда поток фоновой задачи выполнит свою задачу. Позже я вызываю Looper.myLooper().quit()
(также в тестовом потоке), чтобы позволить тестовому потоку выйти из loop
и возобновить логику тестирования.
Все отлично и денди, пока я не захочу написать несколько тестовых методов.
Проблема заключается в том, что Looper, похоже, не предназначен для разрешения выхода и перезапуска в одном потоке, поэтому я вынужден выполнять все мои тесты внутри одного тестового метода.
Я заглянул в исходный код Looper и не смог найти способ обойти его.
Есть ли другой способ проверить мой код Hander/Looper? Или, может быть, еще один удобный способ написать мой фоновый класс задач?
Ответы
Ответ 1
Исходный код Looper показывает, что Looper.myLooper(). quit() помещает нулевое сообщение в очередь сообщений, которое сообщает Looper, что он обрабатывает сообщения FOREVER. По существу, поток становится мертвой нитью в этот момент, и нет никакого способа оживить его, о котором я знаю. Возможно, вы видели сообщения об ошибках при попытке отправить сообщения на
Обработчик после quit() вызывается эффекту "попытка отправить сообщение в мертвую нить". Вот что это значит.
Ответ 2
Это может быть легко протестировано, если вы не используете AsyncTask
, введя второй поток петлителя (кроме основного, созданного для вас неявно Android). Основная стратегия состоит в том, чтобы заблокировать поток основного цикла с помощью CountDownLatch
, делегируя все ваши обратные вызовы во второй поток петлителя.
Предостережение здесь заключается в том, что ваш тестируемый код должен иметь возможность поддерживать использование петлителя, отличного от основного по умолчанию. Я бы сказал, что это должно быть так, независимо от поддержки более надежного и гибкого дизайна, и это также очень легко. В общем, все, что нужно сделать, - это изменить свой код, чтобы принять необязательный параметр 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
Я наткнулся на ту же проблему, что и ваша. Я также хотел создать тестовый пример для класса, использующего Handler
.
То же, что и вы, я использую Looper.loop()
, чтобы тестовый поток начал обрабатывать очереди в обработчике.
Чтобы остановить его, я использовал реализацию MessageQueue.IdleHandler
, чтобы уведомить меня, когда петлитель блокирует ожидание следующего следующего сообщения. Когда это произойдет, я вызываю метод quit()
. Но опять же, как и у меня, у меня возникла проблема, когда я делаю несколько тестов.
Интересно, решили ли вы эту проблему и, возможно, захотите поделиться ею со мной (и, возможно, с другими):)
PS: Я также хотел бы знать, как вы называете свой Looper.myLooper().quit()
.
Спасибо!
Ответ 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);
}
}
Теперь в моих тестах (которые нужно запускать в потоке пользовательского интерфейса) теперь я могу сделать:
@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(...)
, нарушат тесты, что не так.