Prueba de Android AsyncTask con Android Test Framework

97

Tengo un ejemplo de implementación de AsyncTask muy simple y tengo problemas para probarlo con el marco de Android JUnit.

Funciona bien cuando lo instancia y lo ejecuto en una aplicación normal. Sin embargo, cuando se ejecuta desde cualquiera de las clases del marco de pruebas de Android (es decir , AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 , etc.) se comporta de manera extraña:

  • Ejecuta el doInBackground()método correctamente
  • Sin embargo, no invoca ninguna de sus métodos de notificación ( onPostExecute(), onProgressUpdate(), etc) - sólo los ignora en silencio sin mostrar ningún error.

Este es un ejemplo de AsyncTask muy 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");
}

}

Este es un caso de prueba. Aquí AsyncTaskDemoActivity es una actividad muy simple que proporciona una interfaz de usuario para probar AsyncTask en el modo:

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

}

Todo este código funciona bien, excepto el hecho de que AsyncTask no invoca sus métodos de notificación cuando se ejecuta dentro de Android Testing Framework. ¿Algunas ideas?

Vladimir Kroz
fuente

Respuestas:

125

Encontré un problema similar al implementar alguna prueba unitaria. Tuve que probar algún servicio que funcionaba con los ejecutores, y necesitaba sincronizar mis devoluciones de llamada de servicio con los métodos de prueba de mis clases de ApplicationTestCase. Por lo general, el método de prueba finaliza antes de que se acceda a la devolución de llamada, por lo que los datos enviados a través de las devoluciones de llamada no se probarán. Intenté aplicar el busto @UiThreadTest y todavía no funcionó.

Encontré el siguiente método, que funcionó, y todavía lo uso. Simplemente uso los objetos de señal CountDownLatch para implementar el mecanismo de espera-notificación (puede usar sincronizado (bloquear) {... lock.notify ();}, sin embargo, esto da como resultado un código feo) mecanismo.

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
fuente
1
¿Qué es Service.doSomething()?
Peter Ajtai
11
Estoy probando una AsynchTask. Hice esto, y, bueno, la tarea en segundo plano parece que nunca se llamará y singnal espera para siempre :(
Ixx
@Ixx, ¿has llamado task.execute(Param...)antes await()y poner countDown()en onPostExecute(Result)? (ver stackoverflow.com/a/5722193/253468 ) También @PeterAjtai, Service.doSomethinges una llamada asíncrona como task.execute.
TWiStErRob
qué solución tan hermosa y sencilla.
Maciej Beimcik
Service.doSomething()es donde debe reemplazar su servicio / llamada de tarea asíncrona. Asegúrese de llamar signal.countDown()a cualquier método que necesite implementar o su prueba se bloqueará.
Victor R. Oliveira
94

Encontré muchas respuestas cercanas, pero ninguna reunió todas las partes correctamente. Entonces, esta es una implementación correcta cuando se usa android.os.AsyncTask en sus casos de prueba 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
fuente
1
gracias por escribir un ejemplo completo ... Estaba teniendo muchos pequeños problemas al implementar esto.
Peter Ajtai
Poco más de 1 año después, y me salvaste. ¡Gracias Billy Brackeen!
2012
8
Si desea que un tiempo de espera cuente como una prueba fallida, puede hacer lo siguiente:assertTrue(signal.await(...));
Jarett Millard
4
Hola Billy, he probado esta implementación pero no se encuentra runTestOnUiThread. ¿Debe el caso de prueba extender AndroidTestCase o necesita extender ActivityInstrumentationTestCase2?
Doug Ray
3
@DougRay Tuve el mismo problema: si extiendes InstrumentationTestCase, se encontrará runTestOnUiThread.
Luminaria
25

La forma de lidiar con esto es ejecutar cualquier código que invoque una AsyncTask en 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());
}

De forma predeterminada, junit ejecuta las pruebas en un hilo distinto al de la interfaz de usuario de la aplicación principal. La documentación de AsyncTask dice que la instancia de la tarea y la llamada a execute () deben estar en el hilo principal de la IU; esto se debe a que AsyncTask depende del hilo principal Loopery MessageQueuede que su controlador interno funcione correctamente.

NOTA:

Anteriormente recomendé usar @UiThreadTestcomo decorador en el método de prueba para forzar que la prueba se ejecute en el hilo principal, pero esto no es del todo correcto para probar una AsyncTask porque mientras su método de prueba se está ejecutando en el hilo principal, no se procesan mensajes en el Main MessageQueue: incluidos los mensajes que AsyncTask envía sobre su progreso, lo que hace que la prueba se cuelgue.

Alex Pretzlav
fuente
Eso me salvó ... aunque tuve que llamar a "runTestOnUiThread" desde otro hilo, de lo contrario, obtendría "Este método no se puede llamar desde el hilo principal de la aplicación"
Matthieu
@Matthieu ¿Estabas usando runTestOnUiThread()un método de prueba con el @UiThreadTestdecorador? Eso no funcionará. Si un método de prueba no lo tiene @UiThreadTest, debe ejecutarse en su propio subproceso no principal de forma predeterminada.
Alex Pretzlav
1
Esa respuesta es pura joya. Debería ser reelaborado para enfatizar la actualización, si realmente desea mantener la respuesta inicial, póngala como una explicación de fondo y un error común.
Snicolas
1
La documentación indica el método Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras
1
En desuso, ¡utilícelo InstrumentationRegistry.getInstrumentation().runOnMainSync()en su lugar!
Marco7757
5

Si no le importa ejecutar AsyncTask en el hilo de la persona que llama (debería estar bien en el caso de las pruebas unitarias), puede usar un Ejecutor en el hilo actual como se describe en https://stackoverflow.com/a/6583868/1266123

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

Y luego ejecuta su AsyncTask en su prueba unitaria de esta manera

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

Esto solo funciona para HoneyComb y versiones posteriores.

robUx4
fuente
esto debería subir
StefanTo
5

Escribí suficientes conjuntos para Android y solo quiero compartir cómo hacerlo.

En primer lugar, aquí está la clase de ayudante responsable de esperar y liberar al camarero. Nada especial:

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

A continuación, creemos una interfaz con un método que se debe llamar desde AsyncTaskque se realiza el trabajo. Seguro que también queremos probar nuestros resultados:

TestTaskItf

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

A continuación, creemos un esqueleto de nuestra tarea que probaremos:

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

Por fin, nuestra clase más unitaria:

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

Eso es todo.

Espero que ayude a alguien.

Maxim Shoustin
fuente
4

Esto se puede utilizar si desea probar el resultado del doInBackgroundmétodo. Anule el onPostExecutemétodo y realice las pruebas allí. Para esperar a que AsyncTask se complete, use CountDownLatch. La latch.await()espera hasta que la cuenta regresiva va de 1 (que se establece durante la inicialización) a 0 (que se realiza mediante el countdown()método).

@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
fuente
-1

La mayoría de esas soluciones requieren que se escriba mucho código para cada prueba o para cambiar la estructura de su clase. Lo cual encuentro muy difícil de usar si tienes muchas situaciones bajo prueba o muchas AsyncTasks en tu proyecto.

Hay una biblioteca que facilita el proceso de prueba AsyncTask. Ejemplo:

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

Básicamente, ejecuta su AsyncTask y prueba el resultado que devuelve después de que postComplete()se haya llamado a.

Leonardo Soares e Silva
fuente