¿Cómo probar mejor el código Looper y Handler de la unidad en Android?

Utilizo la clase android.os.Handler para realizar tareas en segundo plano. Cuando la unidad de pruebas de estos, Llamo Looper.loop() para hacer el hilo de prueba esperar a que el hilo de la tarea de fondo hacer lo suyo. Más tarde, llamo Looper.myLooper().quit() (también en el hilo de prueba), para permitir que el hilo de prueba salga del loop y reanude la lógica de prueba.

Está todo bien y dandy hasta que quiero escribir más de un método de prueba.

El problema es que Looper no parece estar diseñado para permitir dejar de fumar y reiniciar en el mismo hilo, por lo que estoy obligado a hacer todas mis pruebas dentro de un solo método de prueba.

Miré en el código fuente de Looper, y no pude encontrar una manera de evitarlo.

¿Hay alguna otra manera de probar mi código de Hander / Looper? O tal vez un poco más de prueba de manera amigable para escribir mi clase de tareas de fondo?

El código fuente de Looper revela que Looper.myLooper (). Quit () encuerda un mensaje nulo en la cola Mensaje, lo que indica a Looper que se realiza procesando mensajes FOREVER. Esencialmente, el hilo se convierte en un hilo muerto en ese punto, y no hay manera de revivirlo que yo sepa. Es posible que haya visto mensajes de error cuando intenta enviar mensajes al Handler después de quit () se llama al efecto "intentar enviar mensaje a hilo muerto". Eso es lo que significa.

Esto puede ser probado fácilmente si no está utilizando AsyncTask introduciendo un segundo hilo de looper (que no sea el principal creado para usted implícitamente por Android). La estrategia básica entonces es bloquear el hilo principal del looper usando un CountDownLatch mientras que delega todas sus callbacks al segundo hilo del looper.

La advertencia aquí es que su código bajo prueba debe ser capaz de soportar el uso de un looper que no sea el principal por defecto. Yo diría que este debe ser el caso independientemente de apoyar un diseño más robusto y flexible, y también es afortunadamente muy fácil. En general, todo lo que debe hacerse es modificar su código para aceptar un parámetro Looper opcional y usarlo para construir su Handler (como new Handler(myLooper) ). Para AsyncTask , este requisito hace imposible probarlo con este enfoque. Un problema que creo que debería ser remediado con AsyncTask sí mismo.

Algún código de ejemplo para empezar:

 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); } 

He tropezado en el mismo problema que el tuyo. También quería hacer un caso de prueba para una clase que utiliza un Handler .

Igual que lo que hiciste, utilizo Looper.loop() para que el subproceso de prueba comience a manejar los mensajes en cola en el controlador.

Para detenerlo, utilicé la implementación de MessageQueue.IdleHandler para notificarme cuando el looper está bloqueando para esperar el próximo mensaje. Cuando sucede, llamo al método quit() . Pero de nuevo, lo mismo que usted tengo un problema cuando hago más de un caso de prueba.

Me pregunto si ya has resuelto este problema y tal vez cuidar de compartirlo conmigo (y posiblemente otros) 🙂

PS: También me gustaría saber cómo llamar a su Looper.myLooper().quit() .

¡Gracias!

Inspirado en la respuesta de @Josh Guilfoyle, decidí tratar de usar la reflexión para tener acceso a lo que necesitaba para hacer mi propio Looper.loop() no bloqueador y sin dejar de fumar 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); } } 

Ahora en mis pruebas (que deben ejecutarse en el hilo de interfaz de usuario), ahora puedo hacer:

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

No estoy diciendo que es una buena idea, pero hasta ahora ha estado funcionando para mí. ¡Vale la pena probarlo! Lo que me gusta de esto es que las Exceptions que se lanzan dentro de algunos hander.post(...) se rompen las pruebas, que no es el caso de lo contrario.

  • Eliminación de contactos (crudos) en Android 2.x
  • Prueba unitaria de interfaz de usuario asíncrono con Espresso, IdlingResource falla con condición de carrera
  • Automatización de la caja de prueba de la unidad Android: biblioteca Robolectric vs marco de pruebas de Android
  • `RenamingDelegatingContext` está obsoleto. ¿Cómo probamos SQLite db ahora?
  • Sugar ORM necesita registros para ser guardado cada vez mientras que la prueba de unidad?
  • ¿Hay alguien por ahí usando Robolectric sin Maven en IntelliJ?
  • ¿Por qué Robotium es más lento cuando realiza tareas sencillas de interfaz de usuario en comparación con el código nativo de Android?
  • Pruebas de escritura para aplicaciones Flow and Mortar
  • Ejecución de pruebas unitarias antes de cada construcción en Gradle
  • Pruebas de unidades de Android fallan para tipo de compilación de depuración
  • ¿Cómo ejecutar pruebas de unidad con todos los permisos de Android M concedidos?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.