2010-09-07 15 views
13

Używam klasy android.os.Handler do wykonywania zadań w tle. Kiedy testuję je, wywołuję Looper.loop(), aby wątek testowy czekał, aż wątek zadania w tle wykona swoje zadanie. Później nazywam Looper.myLooper().quit() (również w wątku testowym), aby umożliwić wątkowi testowemu wyjście z loop i wznowienie logiki testowania.Jak lepiej testować urządzenie Looper i kod obsługi w systemie Android?

Wszystko w porządku i dandysa, póki nie chcę napisać więcej niż jednej metody testowej.

Problem polega na tym, że Looper nie jest zaprojektowany, aby umożliwić przerwanie i ponowne uruchomienie w tym samym wątku, więc jestem zmuszony wykonać wszystkie moje testy w ramach jednej metody testowej.

Zajrzałem do kodu źródłowego programu Looper i nie mogłem go obejść.

Czy istnieje inny sposób sprawdzenia mojego kodu Hander/Looper? A może jakiś bardziej przyjazny dla testu sposób napisania mojej podstawowej klasy zadań?

+0

Czy możesz dodać do tego przykładowy kod? Mam zasadniczo to samo pytanie, z wyjątkiem tego, że nie dotarłem tak daleko jak ty. –

Odpowiedz

0

Natknąłem się na ten sam problem, co twój. Chciałem również zrobić test dla klasy, która używa Handler.

Tak samo, jak to, co zrobiłeś, używam Looper.loop(), aby wątek testowy zaczął obsługiwać kolejkowane wiadomości w procedurze obsługi.

Aby temu zapobiec, skorzystałem z implementacji MessageQueue.IdleHandler, aby powiadomić mnie, gdy looper blokuje oczekiwanie na następną wiadomość. Kiedy to się dzieje, nazywam metodę quit(). Ale znowu, tak samo jak ty, mam problem, gdy wykonuję więcej niż jeden test testowy.

Zastanawiam się, czy już rozwiązał ten problem i może dbać aby podzielić się ze mną (i ewentualnie innych) :)

PS: Ja też chciałbym wiedzieć, w jaki sposób skontaktować się z Looper.myLooper().quit().

Dzięki!

3

Kod źródłowy Looper'a ujawnia, że ​​Looper.myLooper(). Quit() wpisuje pusty komunikat do kolejki komunikatów, który informuje Looper, że przetwarza komunikaty FOREVER. Zasadniczo wątek staje się martwą nicią w tym punkcie i nie ma sposobu, aby ożywić to, co wiem. Możliwe, że podczas próby wysłania wiadomości do zobaczysz komunikaty o błędach po wywołaniu quit() w celu "próby wysłania wiadomości do martwego wątku". To właśnie oznacza.

2

Można to łatwo przetestować, jeśli nie używasz AsyncTask, wprowadzając drugi wątek wątku (inny niż główny utworzony domyślnie przez system Android). Podstawową strategią jest wówczas zablokowanie głównego wątku programu looper za pomocą CountDownLatch, podczas delegowania wszystkich wywołań zwrotnych do drugiego wątku loopera.

Ograniczeniem jest to, że testowany kod musi być w stanie obsłużyć przy użyciu programu sprzęgającego innego niż domyślny główny. Twierdzę, że tak powinno być, niezależnie od tego, aby wspierać bardziej solidny i elastyczny projekt, a na szczęście jest to również bardzo łatwe. Ogólnie rzecz biorąc, wszystko, co należy zrobić, to zmodyfikować kod, aby zaakceptować opcjonalny parametr Looper i użyć go do skonstruowania swojej Handler (jako). W przypadku AsyncTask wymóg ten uniemożliwia przetestowanie go za pomocą tego podejścia. Problem, który moim zdaniem powinien zostać naprawiony sam z AsyncTask.

Niektóre przykładowy kod, aby dostać się zaczęło:

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

Zainspirowany odpowiedź @Josh Guilfoyle, postanowiłem spróbować użyć refleksji, aby uzyskać dostęp do tego, co konieczne w celu uczynienia własną nieblokujące a nie rzucający palenia 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); 
    } 
} 

Teraz w moich testów (które muszą działać na wątku UI), mogę teraz zrobić:

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

Nie mówię, że to jest dobry pomysł, ale do tej pory to było pracuję dla mnie. Może warto spróbować! To, co podoba mi się w tym, to to, że Exceptions, które są wyrzucane wewnątrz jakiegoś hander.post(...), przerwie testy, co nie jest prawdą.

Powiązane problemy