Testen von Android AsyncTask mit Android Test Framework

97

Ich habe ein sehr einfaches AsyncTask-Implementierungsbeispiel und habe Probleme beim Testen mit dem Android JUnit-Framework.

Es funktioniert einwandfrei, wenn ich es in einer normalen Anwendung instanziiere und ausführe. Wenn es jedoch von einer der Android Testing-Framework-Klassen (z. B. AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 usw.) ausgeführt wird, verhält es sich seltsam:

  • Die doInBackground()Methode wird korrekt ausgeführt
  • Allerdings ist es nicht eine ihrer Benachrichtigungsmethoden aufruft ( onPostExecute(), onProgressUpdate()usw.) - nur still ignoriert sie ohne Fehler zeigt.

Dies ist ein sehr einfaches AsyncTask-Beispiel:

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

}

Dies ist ein Testfall. Hier ist AsyncTaskDemoActivity eine sehr einfache Aktivität, die eine Benutzeroberfläche zum Testen von AsyncTask im Modus bereitstellt:

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

}

Der gesamte Code funktioniert einwandfrei, mit Ausnahme der Tatsache, dass AsyncTask seine Benachrichtigungsmethoden nicht aufruft, wenn es von Android Testing Framework ausgeführt wird. Irgendwelche Ideen?

Vladimir Kroz
quelle

Antworten:

125

Ich habe ein ähnliches Problem bei der Implementierung eines Unit-Tests festgestellt. Ich musste einen Dienst testen, der mit Executors zusammenarbeitete, und meine Dienstrückrufe mussten mit den Testmethoden aus meinen ApplicationTestCase-Klassen synchronisiert werden. Normalerweise wurde die Testmethode selbst beendet, bevor auf den Rückruf zugegriffen wurde, sodass die über die Rückrufe gesendeten Daten nicht getestet wurden. Der Versuch, die @ UIThreadTest-Büste anzuwenden, hat immer noch nicht funktioniert.

Ich habe die folgende Methode gefunden, die funktioniert hat, und ich benutze sie immer noch. Ich verwende einfach CountDownLatch-Signalobjekte, um den Wait-Notify-Mechanismus zu implementieren (Sie können synchronized (lock) {... lock.notify ();} verwenden, dies führt jedoch zu hässlichem Code).

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
quelle
1
Was ist Service.doSomething()?
Peter Ajtai
11
Ich teste eine AsynchTask. Hat dies getan, und nun, die Hintergrundaufgabe scheint nie aufgerufen zu werden und Singnal wartet für immer :(
Ixx
@Ixx, rief sie task.execute(Param...)vor await()und setzen countDown()in onPostExecute(Result)? (siehe stackoverflow.com/a/5722193/253468 ) Auch @PeterAjtai Service.doSomethingist ein asynchroner Aufruf wie task.execute.
TWiStErRob
Was für eine schöne und einfache Lösung.
Maciej Beimcik
Service.doSomething()Hier sollten Sie Ihren Service- / Async-Task-Aufruf ersetzen. Stellen Sie sicher, signal.countDown()dass Sie jede Methode aufrufen , die Sie implementieren müssen, sonst bleibt Ihr Test hängen.
Victor R. Oliveira
94

Ich habe viele enge Antworten gefunden, aber keine hat alle Teile richtig zusammengefügt. Dies ist also eine korrekte Implementierung, wenn Sie eine android.os.AsyncTask in Ihren JUnit-Testfällen verwenden.

 /**
 * 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
quelle
1
Vielen Dank, dass Sie ein vollständiges Beispiel geschrieben haben. Ich hatte viele kleine Probleme bei der Implementierung.
Peter Ajtai
Etwas mehr als ein Jahr später hast du mich gerettet. Vielen Dank, dass Sie Billy Brackeen!
MaTT
8
Wenn Sie möchten, dass eine Zeitüberschreitung als Testfehler gilt, können Sie assertTrue(signal.await(...));
Folgendes
4
Hey Billy, ich habe diese Implementierung ausprobiert, aber runTestOnUiThread wurde nicht gefunden. Sollte der Testfall AndroidTestCase erweitern oder muss ActivityInstrumentationTestCase2 erweitert werden?
Doug Ray
3
@DougRay Ich hatte das gleiche Problem - wenn Sie InstrumentationTestCase erweitern, wird runTestOnUiThread gefunden.
Leuchte
25

Der Weg, um damit umzugehen, besteht darin, jeden Code auszuführen, der eine AsyncTask aufruft 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());
}

Standardmäßig führt junit Tests in einem anderen Thread als der Benutzeroberfläche der Hauptanwendung aus. In der Dokumentation von AsyncTask heißt es, dass sich die Taskinstanz und der Aufruf von execute () im Haupt-UI-Thread befinden müssen. Dies liegt daran, dass AsyncTask vom Hauptthread abhängt Looperund MessageQueuedass sein interner Handler ordnungsgemäß funktioniert.

HINWEIS:

Ich habe zuvor empfohlen, @UiThreadTestdie Testmethode als Dekorator zu verwenden, um zu erzwingen, dass der Test auf dem Hauptthread ausgeführt wird. Dies ist jedoch nicht ganz richtig, um eine AsyncTask zu testen, da während die Testmethode auf dem Hauptthread ausgeführt wird, keine Nachrichten auf dem Test verarbeitet werden main MessageQueue - einschließlich der Nachrichten, die die AsyncTask über ihren Fortschritt sendet, wodurch Ihr Test hängen bleibt.

Alex Pretzlav
quelle
Das hat mich gerettet ... obwohl ich "runTestOnUiThread" von einem anderen Thread aufrufen musste, würde ich sonst "Diese Methode kann nicht vom Hauptanwendungsthread aufgerufen werden" erhalten
Matthieu
@Matthieu Haben Sie runTestOnUiThread()eine Testmethode mit dem @UiThreadTestDekorateur verwendet? Das wird nicht funktionieren. Wenn eine Testmethode nicht vorhanden ist @UiThreadTest, sollte sie standardmäßig auf einem eigenen Nicht-Hauptthread ausgeführt werden.
Alex Pretzlav
1
Diese Antwort ist ein Juwel. Es sollte überarbeitet werden, um das Update hervorzuheben. Wenn Sie die erste Antwort wirklich behalten möchten, geben Sie sie als Hintergrunderklärung und als häufige Gefahr an.
Snicolas
1
Die Dokumentation gibt die Methode Deprecated in API level 24 developer.android.com/reference/android/test/… an
Aivaras
1
Veraltet, InstrumentationRegistry.getInstrumentation().runOnMainSync()stattdessen verwenden!
Marco7757
5

Wenn es Ihnen nichts ausmacht, die AsyncTask im Aufruferthread auszuführen (sollte im Falle von Unit-Tests in Ordnung sein), können Sie im aktuellen Thread einen Executor verwenden, wie unter https://stackoverflow.com/a/6583868/1266123 beschrieben

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

Und dann führen Sie Ihre AsyncTask in Ihrem Unit-Test so aus

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

Dies funktioniert nur für HoneyComb und höher.

robUx4
quelle
Dies sollte steigen
StefanTo
5

Ich habe genug Unitests für Android geschrieben und möchte nur mitteilen, wie das geht.

Zunächst einmal ist hier die Helferklasse, die dafür verantwortlich ist, zu warten und den Kellner freizulassen. Nichts Besonderes:

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

Als nächstes erstellen wir eine Schnittstelle mit einer Methode, von der aus aufgerufen werden soll AsyncTask wenn die Arbeit erledigt ist. Natürlich wollen wir auch unsere Ergebnisse testen:

TestTaskItf

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

Als nächstes erstellen wir ein Skelett unserer Aufgabe, das wir testen werden:

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

Endlich - unsere einheitlichste Klasse:

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

Das ist alles.

Hoffe es wird jemandem helfen.

Maxim Shoustin
quelle
4

Dies kann verwendet werden, wenn Sie das Ergebnis der doInBackgroundMethode testen möchten . Überschreiben Sie die onPostExecuteMethode und führen Sie die Tests dort durch. Verwenden Sie CountDownLatch, um auf den Abschluss der AsyncTask zu warten. Das latch.await()Warten, bis der Countdown von 1 (der während der Initialisierung eingestellt wird) bis 0 (was von der countdown()Methode durchgeführt wird) läuft .

@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.
quelle
-1

Die meisten dieser Lösungen erfordern, dass für jeden Test viel Code geschrieben oder Ihre Klassenstruktur geändert wird. Was ich sehr schwierig finde, wenn Sie viele zu testende Situationen oder viele AsyncTasks in Ihrem Projekt haben.

Es gibt eine Bibliothek , die den Testprozess vereinfacht AsyncTask. Beispiel:

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

Grundsätzlich wird Ihre AsyncTask ausgeführt und das Ergebnis getestet, das nach dem postComplete()Aufruf von zurückgegeben wird.

Leonardo Soares und Silva
quelle