Test d'Android AsyncTask avec Android Test Framework

97

J'ai un exemple d'implémentation AsyncTask très simple et j'ai des problèmes pour le tester à l'aide du framework Android JUnit.

Cela fonctionne très bien lorsque je l'instancie et l'exécute dans une application normale. Cependant, lorsqu'il est exécuté à partir de l'une des classes du framework de test Android (par exemple, AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2, etc.), il se comporte étrangement:

  • Il exécute doInBackground()correctement la méthode
  • Cependant , il n'invoque l' une de ses méthodes de notification ( onPostExecute(), onProgressUpdate(), etc.) - silencieusement les ignore sans montrer aucune erreur.

Ceci est un exemple AsyncTask très simple:

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

}

Ceci est un cas de test. Ici, AsyncTaskDemoActivity est une activité très simple fournissant une interface utilisateur pour tester AsyncTask en mode:

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

}

Tout ce code fonctionne très bien, sauf le fait qu'AsyncTask n'invoque pas ses méthodes de notification lorsqu'il est exécuté dans Android Testing Framework. Des idées?

Vladimir Kroz
la source

Réponses:

125

J'ai rencontré un problème similaire lors de la mise en œuvre d'un test unitaire. J'ai dû tester un service qui fonctionnait avec les exécuteurs, et j'avais besoin de synchroniser mes rappels de service avec les méthodes de test de mes classes ApplicationTestCase. Habituellement, la méthode de test elle-même s'est terminée avant l'accès au rappel, de sorte que les données envoyées via les rappels ne seraient pas testées. J'ai essayé d'appliquer le buste @UiThreadTest ne fonctionnait toujours pas.

J'ai trouvé la méthode suivante, qui a fonctionné, et je l'utilise toujours. J'utilise simplement des objets de signal CountDownLatch pour implémenter le mécanisme de notification d'attente (vous pouvez utiliser synchronized (lock) {... lock.notify ();}, mais cela entraîne un code laid).

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
}
bandi
la source
1
Qu'est-ce que c'est Service.doSomething()?
Peter Ajtai
11
Je teste une AsynchTask. Fait cela, et bien, la tâche d'arrière-plan semble ne jamais être appelée et singnal attend pour toujours :(
Ixx
@Ixx, avez - vous appelé task.execute(Param...)avant await()et mettre countDown()en onPostExecute(Result)? (voir stackoverflow.com/a/5722193/253468 ) @PeterAjtai, Service.doSomethingest également un appel async comme task.execute.
TWiStErRob
quelle belle et simple solution.
Maciej Beimcik
Service.doSomething()est l'endroit où vous devez remplacer votre appel de tâche de service / asynchrone. Assurez-vous d'appeler signal.countDown()n'importe quelle méthode que vous devez implémenter, sinon votre test restera bloqué.
Victor R. Oliveira
94

J'ai trouvé beaucoup de réponses proches, mais aucune n'a réuni correctement toutes les parties. Il s'agit donc d'une implémentation correcte lors de l'utilisation d'un android.os.AsyncTask dans vos cas de tests 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);
}
Billy Brackeen
la source
1
merci d'avoir écrit un exemple complet ... J'avais beaucoup de petits problèmes à mettre en œuvre.
Peter Ajtai
Un peu plus d'un an plus tard, et vous m'avez sauvé. Merci Billy Brackeen!
MaTT
8
Si vous voulez qu'un timeout compte comme un échec de test, vous pouvez faire:assertTrue(signal.await(...));
Jarett Millard
4
Hey Billy, j'ai essayé cette implémentation mais runTestOnUiThread n'est pas trouvé. Le scénario de test doit-il étendre AndroidTestCase ou doit-il étendre ActivityInstrumentationTestCase2?
Doug Ray
3
@DougRay J'ai eu le même problème - si vous étendez InstrumentationTestCase, runTestOnUiThread sera trouvé.
Luminaire le
25

La façon de gérer cela est d'exécuter n'importe quel code qui appelle une AsyncTask dans 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());
}

Par défaut, junit exécute les tests dans un thread distinct de l'interface utilisateur principale de l'application. La documentation d'AsyncTask indique que l'instance de tâche et l'appel à execute () doivent être sur le thread d'interface utilisateur principal; en effet, AsyncTask dépend du thread principal Looperet MessageQueuedu bon fonctionnement de son gestionnaire interne.

REMARQUE:

J'ai précédemment recommandé d'utiliser @UiThreadTestcomme décorateur sur la méthode de test pour forcer le test à s'exécuter sur le thread principal, mais ce n'est pas tout à fait correct pour tester une AsyncTask car pendant que votre méthode de test s'exécute sur le thread principal, aucun message n'est traité sur le Main MessageQueue - y compris les messages envoyés par AsyncTask sur sa progression, provoquant le blocage de votre test.

Alex Pretzlav
la source
Cela m'a sauvé ... même si j'ai dû appeler "runTestOnUiThread" à partir d'un autre thread, sinon, j'obtiendrais "Cette méthode ne peut pas être appelée depuis le thread principal de l'application"
Matthieu
@Matthieu Utilisiez-vous à runTestOnUiThread()partir d'une méthode de test avec le @UiThreadTestdécorateur? Cela ne fonctionnera pas. Si une méthode de test n'en a pas @UiThreadTest, elle doit s'exécuter par défaut sur son propre thread non principal.
Alex Pretzlav
1
Cette réponse est un pur bijou. Il devrait être retravaillé pour mettre l'accent sur la mise à jour, si vous voulez vraiment garder la réponse initiale, mettez-la comme une explication de fond et un écueil courant.
Snicolas
1
La documentation indique la méthode Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras
1
Obsolète, utilisez InstrumentationRegistry.getInstrumentation().runOnMainSync()plutôt!
Marco7757
5

Si cela ne vous dérange pas d'exécuter AsyncTask dans le thread de l'appelant (cela devrait convenir en cas de test unitaire), vous pouvez utiliser un Executor dans le thread actuel comme décrit dans https://stackoverflow.com/a/6583868/1266123

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

Et puis vous exécutez votre AsyncTask dans votre test unitaire comme ceci

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

Cela ne fonctionne que pour HoneyComb et supérieur.

robUx4
la source
cela devrait augmenter
Stefan Jusqu'au
5

J'ai écrit suffisamment de tests unitaires pour Android et je veux juste partager comment faire cela.

Tout d'abord, voici la classe d'assistance chargée d'attendre et de libérer le serveur. Rien de spécial:

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

Ensuite, créons une interface avec une méthode qui devrait être appelée à partir du AsyncTaskmoment où le travail est terminé. Bien sûr, nous voulons également tester nos résultats:

TestTaskItf

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

Ensuite, créons un squelette de notre tâche que nous allons tester:

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

Enfin - notre classe unitaire:

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

C'est tout.

J'espère que cela aidera quelqu'un.

Maxim Shoustin
la source
4

Cela peut être utilisé si vous souhaitez tester le résultat de la doInBackgroundméthode. Remplacez la onPostExecuteméthode et effectuez les tests là-bas. Pour attendre la fin de la tâche AsyncTask, utilisez CountDownLatch. Les latch.await()temps d' attente jusqu'à ce que le compte à rebours se déroule du 1 (qui est définie lors de l' initialisation) à 0 ( ce qui est fait par la countdown()méthode).

@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();
    }
Keerthana S
la source
-1

La plupart de ces solutions nécessitent beaucoup de code à écrire pour chaque test ou pour modifier la structure de votre classe. Ce que je trouve très difficile à utiliser si vous avez de nombreuses situations en cours de test ou de nombreuses AsyncTasks sur votre projet.

Il existe une bibliothèque qui facilite le processus de test AsyncTask. Exemple:

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

Fondamentalement, il exécute votre AsyncTask et teste le résultat qu'il renvoie après l' postComplete()appel de.

Leonardo Soares e Silva
la source