Test di Android AsyncTask con Android Test Framework


97

Ho un esempio di implementazione AsyncTask molto semplice e ho problemi a testarlo utilizzando il framework JUnit Android.

Funziona perfettamente quando creo un'istanza e lo eseguo in una normale applicazione. Tuttavia, quando viene eseguito da una qualsiasi delle classi del framework Android Testing (ad esempio AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 ecc.) Si comporta in modo strano:

  • Esegue il doInBackground()metodo correttamente
  • Tuttavia non si invoca uno qualsiasi dei suoi metodi di notifica ( onPostExecute(), onProgressUpdate(), ecc) - li ignora semplicemente in silenzio, senza mostrare alcun errore.

Questo è un esempio di AsyncTask molto semplice:

package kroz.andcookbook.threads.asynctask;

import android.os.AsyncTask;
import android.util.Log;
import android.widget.ProgressBar;
import android.widget.Toast;

public class AsyncTaskDemo extends AsyncTask<Integer, Integer, String> {

AsyncTaskDemoActivity _parentActivity;
int _counter;
int _maxCount;

public AsyncTaskDemo(AsyncTaskDemoActivity asyncTaskDemoActivity) {
    _parentActivity = asyncTaskDemoActivity;
}

@Override
protected void onPreExecute() {
    super.onPreExecute();
    _parentActivity._progressBar.setVisibility(ProgressBar.VISIBLE);
    _parentActivity._progressBar.invalidate();
}

@Override
protected String doInBackground(Integer... params) {
    _maxCount = params[0];
    for (_counter = 0; _counter <= _maxCount; _counter++) {
        try {
            Thread.sleep(1000);
            publishProgress(_counter);
        } catch (InterruptedException e) {
            // Ignore           
        }
    }
}

@Override
protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    int progress = values[0];
    String progressStr = "Counting " + progress + " out of " + _maxCount;
    _parentActivity._textView.setText(progressStr);
    _parentActivity._textView.invalidate();
}

@Override
protected void onPostExecute(String result) {
    super.onPostExecute(result);
    _parentActivity._progressBar.setVisibility(ProgressBar.INVISIBLE);
    _parentActivity._progressBar.invalidate();
}

@Override
protected void onCancelled() {
    super.onCancelled();
    _parentActivity._textView.setText("Request to cancel AsyncTask");
}

}

Questo è un caso di prova. Qui AsyncTaskDemoActivity è un'attività molto semplice che fornisce l'interfaccia utente per testare AsyncTask in modalità:

package kroz.andcookbook.test.threads.asynctask;
import java.util.concurrent.ExecutionException;
import kroz.andcookbook.R;
import kroz.andcookbook.threads.asynctask.AsyncTaskDemo;
import kroz.andcookbook.threads.asynctask.AsyncTaskDemoActivity;
import android.content.Intent;
import android.test.ActivityUnitTestCase;
import android.widget.Button;

public class AsyncTaskDemoTest2 extends ActivityUnitTestCase<AsyncTaskDemoActivity> {
AsyncTaskDemo _atask;
private Intent _startIntent;

public AsyncTaskDemoTest2() {
    super(AsyncTaskDemoActivity.class);
}

protected void setUp() throws Exception {
    super.setUp();
    _startIntent = new Intent(Intent.ACTION_MAIN);
}

protected void tearDown() throws Exception {
    super.tearDown();
}

public final void testExecute() {
    startActivity(_startIntent, null, null);
    Button btnStart = (Button) getActivity().findViewById(R.id.Button01);
    btnStart.performClick();
    assertNotNull(getActivity());
}

}

Tutto questo codice funziona perfettamente, tranne per il fatto che AsyncTask non richiama i suoi metodi di notifica quando viene eseguito da Android Testing Framework. Qualche idea?

Risposte:


125

Ho incontrato un problema simile durante l'implementazione di alcuni test unitari. Ho dovuto testare alcuni servizi che funzionavano con gli Executors e avevo bisogno di sincronizzare i callback dei miei servizi con i metodi di test delle mie classi ApplicationTestCase. Di solito il metodo di test stesso terminava prima dell'accesso al callback, quindi i dati inviati tramite i callback non sarebbero stati testati. Ho provato ad applicare il busto @UiThreadTest ancora non ha funzionato.

Ho trovato il seguente metodo, che ha funzionato, e lo uso ancora. Uso semplicemente gli oggetti segnale CountDownLatch per implementare il meccanismo di attesa notifica (puoi usare sincronizzato (lock) {... lock.notify ();}, tuttavia questo si traduce in un brutto codice).

public void testSomething(){
final CountDownLatch signal = new CountDownLatch(1);
Service.doSomething(new Callback() {

  @Override
  public void onResponse(){
    // test response data
    // assertEquals(..
    // assertTrue(..
    // etc
    signal.countDown();// notify the count down latch
  }

});
signal.await();// wait for callback
}

1
Che cos'è Service.doSomething()?
Peter Ajtai

11
Sto testando un AsynchTask. Fatto questo e, beh, l'attività in background sembra non essere mai stata chiamata e il singolo attende per sempre :(
Ixx

@Ixx, hai chiamato task.execute(Param...)prima await()e mettere countDown()in onPostExecute(Result)? (vedi stackoverflow.com/a/5722193/253468 ) Inoltre @PeterAjtai, Service.doSomethingè una chiamata asincrona come task.execute.
TWiStErRob

che bella e semplice soluzione.
Maciej Beimcik

Service.doSomething()è dove dovresti sostituire la chiamata al servizio / attività asincrona. Assicurati di chiamare signal.countDown()qualsiasi metodo che devi implementare o il tuo test si bloccherà.
Victor R. Oliveira

94

Ho trovato molte risposte vicine ma nessuna di esse ha messo insieme tutte le parti correttamente. Quindi questa è un'implementazione corretta quando si utilizza un android.os.AsyncTask nei casi di test JUnit.

 /**
 * This demonstrates how to test AsyncTasks in android JUnit. Below I used 
 * an in line implementation of a asyncTask, but in real life you would want
 * to replace that with some task in your application.
 * @throws Throwable 
 */
public void testSomeAsynTask () throws Throwable {
    // create  a signal to let us know when our task is done.
    final CountDownLatch signal = new CountDownLatch(1);

    /* Just create an in line implementation of an asynctask. Note this 
     * would normally not be done, and is just here for completeness.
     * You would just use the task you want to unit test in your project. 
     */
    final AsyncTask<String, Void, String> myTask = new AsyncTask<String, Void, String>() {

        @Override
        protected String doInBackground(String... arg0) {
            //Do something meaningful.
            return "something happened!";
        }

        @Override
        protected void onPostExecute(String result) {
            super.onPostExecute(result);

            /* This is the key, normally you would use some type of listener
             * to notify your activity that the async call was finished.
             * 
             * In your test method you would subscribe to that and signal
             * from there instead.
             */
            signal.countDown();
        }
    };

    // Execute the async task on the UI thread! THIS IS KEY!
    runTestOnUiThread(new Runnable() {

        @Override
        public void run() {
            myTask.execute("Do something");                
        }
    });       

    /* The testing thread will wait here until the UI thread releases it
     * above with the countDown() or 30 seconds passes and it times out.
     */        
    signal.await(30, TimeUnit.SECONDS);

    // The task is done, and now you can assert some things!
    assertTrue("Happiness", true);
}

1
grazie per aver scritto un esempio completo ... Avevo molti piccoli problemi nell'implementarlo.
Peter Ajtai

Poco più di un anno dopo e mi hai salvato. Grazie Billy Brackeen!
MaTT

8
Se vuoi che un timeout conti come un fallimento del test, puoi fare:assertTrue(signal.await(...));
Jarett Millard

4
Hey Billy ho provato questa implementazione ma runTestOnUiThread non è stato trovato. Il test case dovrebbe estendere AndroidTestCase o deve estendere ActivityInstrumentationTestCase2?
Doug Ray il

3
@DougRay Ho avuto lo stesso problema: se estendi InstrumentationTestCase, verrà trovato runTestOnUiThread.
Apparecchio

25

Il modo per risolvere questo problema è eseguire qualsiasi codice che richiami un AsyncTask in runTestOnUiThread():

public final void testExecute() {
    startActivity(_startIntent, null, null);
    runTestOnUiThread(new Runnable() {
        public void run() {
            Button btnStart = (Button) getActivity().findViewById(R.id.Button01);
            btnStart.performClick();
        }
    });
    assertNotNull(getActivity());
    // To wait for the AsyncTask to complete, you can safely call get() from the test thread
    getActivity()._myAsyncTask.get();
    assertTrue(asyncTaskRanCorrectly());
}

Per impostazione predefinita, junit esegue i test in un thread separato rispetto all'interfaccia utente dell'applicazione principale. La documentazione di AsyncTask dice che l'istanza dell'attività e la chiamata a execute () devono essere sul thread dell'interfaccia utente principale; questo perché AsyncTask dipende dal thread principale Loopere dal MessageQueuesuo gestore interno per funzionare correttamente.

NOTA:

In precedenza ho consigliato di utilizzare @UiThreadTestcome decoratore sul metodo di test per forzare l'esecuzione del test sul thread principale, ma questo non è del tutto corretto per testare un AsyncTask perché mentre il metodo di test è in esecuzione sul thread principale non vengono elaborati messaggi sul main MessageQueue - inclusi i messaggi che AsyncTask invia sul suo progresso, causando il blocco del test.


Questo mi ha salvato ... anche se ho dovuto chiamare "runTestOnUiThread" da un altro thread altrimenti avrei ottenuto "Questo metodo non può essere chiamato dal thread dell'applicazione principale"
Matthieu

@ Matthieu Usavi runTestOnUiThread()da un metodo di prova con il @UiThreadTestdecoratore? Non funzionerà. Se un metodo di test non ha @UiThreadTest, dovrebbe essere eseguito sul proprio thread non principale per impostazione predefinita.
Alex Pretzlav

1
Quella risposta è un gioiello puro. Dovrebbe essere rielaborato per enfatizzare l'aggiornamento, se vuoi davvero mantenere la risposta iniziale, mettila come spiegazione di fondo e trappola comune.
Snicolas

1
La documentazione afferma il metodo Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras

1
Deprecato, usa InstrumentationRegistry.getInstrumentation().runOnMainSync()invece!
Marco7757

5

Se non ti dispiace eseguire AsyncTask nel thread del chiamante (dovrebbe andare bene in caso di unit test), puoi utilizzare un Executor nel thread corrente come descritto in https://stackoverflow.com/a/6583868/1266123

public class CurrentThreadExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}

E poi esegui il tuo AsyncTask nel tuo unit test in questo modo

myAsyncTask.executeOnExecutor(new CurrentThreadExecutor(), testParam);

Funziona solo per HoneyComb e versioni successive.


questo dovrebbe aumentare
Stefan al

5

Ho scritto abbastanza unitest per Android e voglio solo condividere come farlo.

Prima di tutto, ecco una classe di aiuto che è responsabile di aspettare e rilasciare il cameriere. Niente di speciale:

SyncronizeTalker

public class SyncronizeTalker {
    public void doWait(long l){
        synchronized(this){
            try {
                this.wait(l);
            } catch(InterruptedException e) {
            }
        }
    }



    public void doNotify() {
        synchronized(this) {
            this.notify();
        }
    }


    public void doWait() {
        synchronized(this){
            try {
                this.wait();
            } catch(InterruptedException e) {
            }
        }
    }
}

Successivamente, creiamo un'interfaccia con un metodo che dovrebbe essere chiamato da AsyncTaskquando il lavoro è finito. Certo, vogliamo anche testare i nostri risultati:

TestTaskItf

public interface TestTaskItf {
    public void onDone(ArrayList<Integer> list); // dummy data
}

Successivamente creiamo uno scheletro del nostro compito che testeremo:

public class SomeTask extends AsyncTask<Void, Void, SomeItem> {

   private ArrayList<Integer> data = new ArrayList<Integer>(); 
   private WmTestTaskItf mInter = null;// for tests only

   public WmBuildGroupsTask(Context context, WmTestTaskItf inter) {
        super();
        this.mContext = context;
        this.mInter = inter;        
    }

        @Override
    protected SomeItem doInBackground(Void... params) { /* .... job ... */}

        @Override
    protected void onPostExecute(SomeItem item) {
           // ....

       if(this.mInter != null){ // aka test mode
        this.mInter.onDone(data); // tell to unitest that we finished
        }
    }
}

Finalmente - la nostra classe più unica:

TestBuildGroupTask

public class TestBuildGroupTask extends AndroidTestCase  implements WmTestTaskItf{


    private SyncronizeTalker async = null;

    public void setUP() throws Exception{
        super.setUp();
    }

    public void tearDown() throws Exception{
        super.tearDown();
    }

    public void test____Run(){

         mContext = getContext();
         assertNotNull(mContext);

        async = new SyncronizeTalker();

        WmTestTaskItf me = this;
        SomeTask task = new SomeTask(mContext, me);
        task.execute();

        async.doWait(); // <--- wait till "async.doNotify()" is called
    }

    @Override
    public void onDone(ArrayList<Integer> list) {
        assertNotNull(list);        

        // run other validations here

       async.doNotify(); // release "async.doWait()" (on this step the unitest is finished)
    }
}

È tutto.

Spero che possa aiutare qualcuno.


4

Questo può essere usato se vuoi testare il risultato del doInBackgroundmetodo. Sostituisci il onPostExecutemetodo ed esegui i test da lì. Per attendere il completamento di AsyncTask, utilizzare CountDownLatch. Le latch.await()aspetta che le piste rovescia da 1 (che è impostato durante l'inizializzazione) a 0 (che è fatto dal countdown()metodo).

@RunWith(AndroidJUnit4.class)
public class EndpointsAsyncTaskTest {

    Context context;

    @Test
    public void testVerifyJoke() throws InterruptedException {
        assertTrue(true);
        final CountDownLatch latch = new CountDownLatch(1);
        context = InstrumentationRegistry.getContext();
        EndpointsAsyncTask testTask = new EndpointsAsyncTask() {
            @Override
            protected void onPostExecute(String result) {
                assertNotNull(result);
                if (result != null){
                    assertTrue(result.length() > 0);
                    latch.countDown();
                }
            }
        };
        testTask.execute(context);
        latch.await();
    }

-1

La maggior parte di queste soluzioni richiede la scrittura di molto codice per ogni test o per modificare la struttura della classe. Che trovo molto difficile da usare se hai molte situazioni sotto test o molti AsyncTask nel tuo progetto.

C'è una libreria che facilita il processo di test AsyncTask. Esempio:

@Test
  public void makeGETRequest(){
        ...
        myAsyncTaskInstance.execute(...);
        AsyncTaskTest.build(myAsyncTaskInstance).
                    run(new AsyncTest() {
                        @Override
                        public void test(Object result) {
                            Assert.assertEquals(200, (Integer)result);
                        }
                    });         
  }       
}

Fondamentalmente, esegue il tuo AsyncTask e testa il risultato che restituisce dopo che postComplete()è stato chiamato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.