Android AsyncTask testing con Android Test Framework


Tengo un ejemplo de implementación de AsyncTask muy simple y estoy teniendo problemas para probarlo usando Android JUnit framework.

Funciona muy bien cuando instanciar y ejecutar en la aplicación normal. Sin embargo, cuando se ejecuta desde cualquier de Android marco de Pruebas de clases (es decir, AndroidTestCase, ActivityUnitTestCase, ActivityInstrumentationTestCase2 etc) que se comporta de manera extraña:

  • Ejecuta doInBackground() método correctamente
  • Sin embargo, no invoca ninguno de sus métodos de notificación (onPostExecute(), onProgressUpdate(), etc) just simplemente los ignora silenciosamente sin mostrar ningún error.

Este es un ejemplo muy simple de AsyncTask:

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 interfaz de usuario para probar AsyncTask en 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 está funcionando bien, excepto el hecho de que AsyncTask no invoca su notificación métodos cuando se ejecutan dentro del marco de pruebas de Android. Alguna idea?

Author: Rahul Sharma, 2010-02-24

7 answers

Me encontré con un problema similar al implementar algunas pruebas unitarias. Tuve que probar algún servicio que funcionaba con ejecutores, y necesitaba tener mis devoluciones de llamada de servicio sincronizadas con los métodos de prueba de mis clases ApplicationTestCase. Por lo general, el método de prueba en sí terminó antes de que se accediera a la devolución de llamada, por lo que los datos enviados a través de las devoluciones de llamada no se probarían. Intentó aplicar el busto @UiThreadTest 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 wait-notify(puede usar sincronizado (lock){... bloqueo.notify ();}, sin embargo esto resulta en el mecanismo de código feo).

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
}
 120
Author: bandi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-24 05:26:52

Encontré muchas respuestas cercanas, pero ninguna de ellas juntó todas las partes correctamente. Así que esta es una implementación correcta cuando se utiliza un Android.operativo.AsyncTask en sus casos de pruebas 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);
}
 89
Author: Billy Brackeen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-10 23:33:32

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 subproceso separado de la interfaz de usuario de la aplicación principal. La documentación de AsyncTask dice que la instancia de tarea y la llamada a execute() deben estar en el subproceso principal de la interfaz de usuario; esto se debe a que AsyncTask depende de los subprocesos principales Looper y MessageQueue para que su controlador interno funcione correctamente.

NOTA:

Previamente recomendé usar @UiThreadTest como un decorador en el método de prueba para forzar la prueba a ejecutarse en el subproceso principal, pero esto no es del todo correcto para probar una AsyncTask porque mientras su método de prueba se ejecuta en el subproceso principal no se procesan mensajes en el MessageQueue principal, incluidos los mensajes que la AsyncTask envía sobre su progreso, causando que su prueba se cuelgue.

 24
Author: Alex Pretzlav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-02 16:39:12

Si no te importa ejecutar la AsyncTask en el subproceso de llamada (debería estar bien en caso de pruebas unitarias), puedes usar un ejecutor en el subproceso 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 de unidad como esta

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

Esto solo funciona para HoneyComb y superior.

 5
Author: robUx4,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-21 17:48:03

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

En primer lugar, aquí está la clase ayudante que se encarga de esperar y liberar a 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, vamos a crear una interfaz con un método que debe ser llamado desde AsyncTask cuando el trabajo esté terminado. Claro que también queremos probar nuestros resultados:

TestTaskItf

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

A continuación vamos a crear un esqueleto de nuestra tarea que vamos a prueba:

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.

 5
Author: Maxim Shoustin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-31 21:05:01

Esto se puede usar si desea probar el resultado del método doInBackground. Anule el método onPostExecute y realice las pruebas allí. Para esperar a que la AsyncTask se complete use CountDownLatch. El latch.await() espera hasta que la cuenta atrás se ejecute de 1 (que se establece durante la inicialización) a 0 (que se realiza mediante el método countdown()).

@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();
    }
 3
Author: Keerthana S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-28 02:04:18

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 me resulta muy difícil de usar si tienes muchas situaciones bajo prueba o muchas AsyncTasks en tu proyecto.

Existe una biblioteca que facilita el proceso de pruebas 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 tu AsyncTask y prueba el resultado que devuelve después de que se haya llamado a postComplete().

 -1
Author: Leonardo Soares e Silva,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-15 07:45:49