Teste Android AsyncTask com Android Test Framework

97

Eu tenho um exemplo de implementação de AsyncTask muito simples e estou tendo problemas em testá-lo usando a estrutura Android JUnit.

Ele funciona muito bem quando eu instancio e executo em um aplicativo normal. No entanto, quando é executado a partir de qualquer uma das classes de estrutura do Android Testing (ou seja , AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 etc), ele se comporta de maneira estranha:

  • Executa o doInBackground()método corretamente
  • No entanto, não invoca qualquer de seus métodos de notificação ( onPostExecute(), onProgressUpdate(), etc) - apenas os ignora sem aviso, sem mostrar quaisquer erros.

Este é um exemplo de AsyncTask muito simples:

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 é um caso de teste. Aqui, AsyncTaskDemoActivity é uma Activity muito simples que fornece IU para testar AsyncTask no 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 esse código está funcionando bem, exceto o fato de que AsyncTask não invoca seus métodos de notificação quando executado dentro do Android Testing Framework. Alguma ideia?

Vladimir Kroz
fonte

Respostas:

125

Eu encontrei um problema semelhante ao implementar alguns testes de unidade. Tive que testar alguns serviços que funcionavam com Executores e precisava sincronizar meus retornos de chamada de serviço com os métodos de teste de minhas classes ApplicationTestCase. Normalmente, o próprio método de teste termina antes que o retorno de chamada seja acessado, portanto, os dados enviados por meio dos retornos de chamada não são testados. Tentei aplicar o erro @UiThreadTest e ainda não funcionou.

Encontrei o método a seguir, que funcionou, e ainda o uso. Eu simplesmente uso objetos de sinal CountDownLatch para implementar o mecanismo de espera-notificação (você pode usar synchronized (lock) {... lock.notify ();}, no entanto, isso resulta em código feio).

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
fonte
1
O que é Service.doSomething()?
Peter Ajtai
11
Estou testando um AsynchTask. Fiz isso e, bem, a tarefa em segundo plano parece nunca ser chamada e o singnal espera para sempre :(
Ixx
@Ixx, você ligou task.execute(Param...)antes await()e colocar countDown()em onPostExecute(Result)? (consulte stackoverflow.com/a/5722193/253468 ) Também @PeterAjtai, Service.doSomethingé uma chamada assíncrona como task.execute.
TWiStErRob
que solução adorável e simples.
Maciej Beimcik
Service.doSomething()é onde você deve substituir sua chamada de serviço / tarefa assíncrona. Certifique-se de chamar signal.countDown()qualquer método que você precise implementar ou seu teste travará.
Victor R. Oliveira
94

Encontrei muitas respostas aproximadas, mas nenhuma delas juntou todas as partes corretamente. Portanto, esta é uma implementação correta ao usar um android.os.AsyncTask em seus casos de testes 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
fonte
1
obrigado por escrever um exemplo completo ... Eu estava tendo muitos pequenos problemas para implementar isso.
Peter Ajtai
Pouco mais de 1 ano depois, e você me salvou. Obrigado Billy Brackeen!
MaTT
8
Se quiser que um tempo limite seja considerado uma falha de teste, você pode fazer:assertTrue(signal.await(...));
Jarett Millard
4
Ei, Billy, tentei essa implementação, mas runTestOnUiThread não foi encontrado. O caso de teste deve estender AndroidTestCase ou precisa estender ActivityInstrumentationTestCase2?
Doug Ray
3
@DougRay Eu tive o mesmo problema - se você estender InstrumentationTestCase, runTestOnUiThread será encontrado.
Luminária de
25

A maneira de lidar com isso é executar qualquer código que invoque uma AsyncTask em 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());
}

Por padrão, o junit executa testes em um encadeamento separado da IU do aplicativo principal. A documentação do AsyncTask diz que a instância da tarefa e a chamada para execute () devem estar no thread de IU principal; isso ocorre porque AsyncTask depende do thread principal Loopere MessageQueuede seu manipulador interno para funcionar corretamente.

NOTA:

Eu recomendei anteriormente usar @UiThreadTestcomo decorador no método de teste para forçar o teste a ser executado no thread principal, mas isso não é muito adequado para testar uma AsyncTask porque enquanto seu método de teste está em execução no thread principal, nenhuma mensagem é processada no main MessageQueue - incluindo as mensagens que o AsyncTask envia sobre seu progresso, fazendo com que seu teste trave.

Alex Pretzlav
fonte
Isso me salvou ... embora eu tivesse que chamar "runTestOnUiThread" de outro thread, caso contrário, eu obteria "Este método não pode ser chamado do thread principal do aplicativo"
Matthieu
@Matthieu Você estava usando runTestOnUiThread()um método de teste com o @UiThreadTestdecorador? Isso não vai funcionar. Se um método de teste não tiver @UiThreadTest, ele deve ser executado em seu próprio thread não principal por padrão.
Alex Pretzlav
1
Essa resposta é pura joia. Deve ser retrabalhado para enfatizar a atualização, se você realmente deseja manter a resposta inicial, coloque-a como uma explicação de fundo e uma armadilha comum.
Snicolas
1
A documentação declara o método Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras
1
Obsoleto, use InstrumentationRegistry.getInstrumentation().runOnMainSync()!
Marco7757
5

Se você não se importa em executar o AsyncTask no thread do chamador (deve funcionar no caso de teste de unidade), você pode usar um Executor no thread atual, conforme descrito em https://stackoverflow.com/a/6583868/1266123

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

E então você executa seu AsyncTask em seu teste de unidade assim

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

Isso está funcionando apenas para HoneyComb e superior.

robUx4
fonte
isso deve subir
StefanTo
5

Eu escrevi unitests suficientes para Android e só quero compartilhar como fazer isso.

Em primeiro lugar, aqui está a classe auxiliar responsável por esperar e liberar o garçom. 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 seguir, vamos criar uma interface com um método que deve ser chamado AsyncTaskquando o trabalho for concluído. Claro, também queremos testar nossos resultados:

TestTaskItf

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

Em seguida, vamos criar um esqueleto de nossa tarefa que iremos testar:

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

Enfim - nossa classe unitária:

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

Isso é tudo.

Espero que ajude alguém.

Maxim Shoustin
fonte
4

Isso pode ser usado se você quiser testar o resultado do doInBackgroundmétodo. Substitua o onPostExecutemétodo e execute os testes lá. Para aguardar a conclusão do AsyncTask, use CountDownLatch. As latch.await()esperas até que a contagem regressiva vá de 1 (que é definido durante a inicialização) a 0 (que é feito pelo 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
fonte
-1

A maioria dessas soluções requer que muitos códigos sejam escritos para cada teste ou para alterar a estrutura de sua classe. Que eu acho muito difícil de usar se você tiver muitas situações em teste ou muitas AsyncTasks em seu projeto.

Existe uma biblioteca que facilita o processo de teste AsyncTask. Exemplo:

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

Basicamente, ele executa sua AsyncTask e testa o resultado que retorna após postComplete()ter sido chamado.

Leonardo Soares e Silva
fonte