Как лучше 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(...), нарушат тесты, что не так.