2010-09-07 11 views
13

Uso la clase android.os.Handler para realizar tareas en segundo plano. Cuando la unidad prueba estos, llamo al Looper.loop() para que el hilo de prueba espere a que el hilo de la tarea de fondo haga su trabajo. Más tarde, llamo al 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.¿Cómo mejorar la unidad de prueba del código Looper y Handler en Android?

Todo está bien y excelente hasta que quiera escribir más de un método de prueba.

El problema es que Looper no parece estar diseñado para permitir el cierre y el reinicio en el mismo hilo, por lo que me veo obligado a hacer todas las 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 Hander/Looper? ¿O tal vez alguna forma más amigable para escribir mi clase de tareas en segundo plano?

+0

¿Puedes publicar un código de muestra para esto? Básicamente tengo la misma pregunta, excepto que no he llegado tan lejos como tú. –

Respuesta

0

He tropezado con el mismo problema que el suyo. También quería hacer un caso de prueba para una clase que usa un Handler.

Igual que lo que hizo, utilizo el Looper.loop() para que la cadena 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 a que llegue el siguiente mensaje. Cuando sucede, llamo al método quit(). Pero, de nuevo, al igual que tú, tengo un problema cuando hago más de un caso de prueba.

Me pregunto si ya resuelto este problema y tal vez importa compartirlo conmigo (y posiblemente otros) :)

PD: También me gustaría saber cómo se llama su Looper.myLooper().quit().

Gracias!

3

El código fuente de Looper revela que Looper.myLooper(). Quit() encola un mensaje nulo en la cola de mensajes, que le dice a Looper que ya está procesando los mensajes FOREVER. Esencialmente, el hilo se convierte en un hilo muerto en ese punto, y no hay forma de revivirlo que yo sepa. Es posible que haya visto mensajes de error al intentar publicar mensajes en el controlador después de llamar a "salir" ("intentar enviar un mensaje al hilo muerto"). Eso es lo que eso significa.

2

Esto realmente se puede probar fácilmente si no está utilizando AsyncTask al introducir un segundo hilo looper (que no sea el principal creado para usted implícitamente por Android). La estrategia básica entonces es bloquear el hilo del looper principal usando un CountDownLatch mientras se delegan todas las devoluciones de llamada en el 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 debería ser el caso, independientemente de que sea compatible con un diseño más robusto y flexible, y afortunadamente también es muy fácil. En general, todo lo que debe hacerse es modificar su código para aceptar un parámetro opcional Looper y usarlo para construir su Handler (como new Handler(myLooper)). Para AsyncTask, este requisito hace que sea imposible probarlo con este enfoque. Un problema que creo que debería remediarse con AsyncTask.

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

Inspirado por la respuesta de @ Josh Guilfoyle, decidí tratar de utilizar la reflexión para obtener acceso a lo que necesitaba para hacer mi propia no bloqueante 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 subproceso 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 esto es una buena idea, pero hasta ahora ha sido trabajando para mi ¡Puede valer la pena probarlo! Lo que me gusta de esto es que Exceptions que se arrojan dentro de alguna hander.post(...) se romperán las pruebas, que no es el caso de lo contrario.