Тестування Android AsyncTask з Android Test Framework


97

У мене дуже простий приклад реалізації AsyncTask, і у мене виникають проблеми при тестуванні його за допомогою Android JUnit Framework.

Він працює чудово, коли я інстанціюю та виконую його у звичайному застосуванні. Однак коли він виконується з будь-якого рамового класу Android Testing (наприклад, AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 тощо), він поводиться дивно:

  • Він doInBackground()правильно виконує метод
  • Однак це не викликає якийсь - або з його методів повідомлення ( onPostExecute(), onProgressUpdate(), і т.д.) - просто мовчки ігнорує їх , не показуючи жодних - або помилок.

Це дуже простий приклад 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");
}

}

Це тестовий випадок. Тут AsyncTaskDemoActivity - це дуже проста активність, що забезпечує інтерфейс для тестування AsyncTask в режимі:

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

}

Весь цей код працює чудово, за винятком того, що AsyncTask не посилається на його методи сповіщення, коли вони виконуються в рамках Android Testing Framework. Якісь ідеї?

Відповіді:


125

Я зіткнувся з подібною проблемою під час впровадження деяких одиничних тестів. Мені довелося перевірити деякий сервіс, який працював з Виконавцями, і мені потрібно було синхронізувати свої зворотні виклики служб із методами тестування з моїх класів ApplicationTestCase. Зазвичай сам тестовий метод закінчується до доступу до зворотного дзвінка, тому дані, що надсилаються через зворотні виклики, не будуть перевірені. Спробував застосувати бюст @UiThreadTest досі не працював.

Я знайшов наступний метод, який спрацював, і досі його використовую. Я просто використовую об'єкти сигналу CountDownLatch для реалізації повідомлення про очікування (ви можете використовувати синхронізований (замок) {... lock.notify ();}, однак це призводить до некрасивого коду) механізму.

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
}

1
Що таке Service.doSomething()?
Peter Ajtai

11
Я тестую AsynchTask. Зробив це, і, мабуть, фонове завдання, здається, ніколи не називатиметься, а сингулярне чекає вічно :(
Ixx,

@Ixx, ви назвали , task.execute(Param...)перш ніж await()і покласти countDown()в onPostExecute(Result)? (Див stackoverflow.com/a/5722193/253468 ) Також @PeterAjtai, Service.doSomethingє асинхронної виклик , як task.execute.
TWiStErRob

яке прекрасне і просте рішення.
Maciej Beimcik

Service.doSomething()саме там слід замінити виклик службового / асинхронного завдання. Не забудьте зателефонувати signal.countDown()за будь-яким методом, який потрібно застосувати, або ваш тест застрягне.
Віктор Р. Олівейра

94

Я знайшов багато близьких відповідей, але жодна з них не розставила всі частини правильно. Отже, це одна правильна реалізація при використанні android.os.AsyncTask у своїх тестах 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);
}

1
дякую за написання повного прикладу ... У мене виникло багато невеликих проблем при здійсненні цього.
Пітер Айтай

Трохи понад 1 рік, і ти врятував мене. Дякую Біллі Браккіну!
MaTT

8
Якщо ви хочете, щоб тайм-аут вважався пробним тестом, ви можете зробити:assertTrue(signal.await(...));
Jarett Millard

4
Привіт Біллі. Я спробував цю реалізацію, але runTestOnUiThread не знайдено. Чи повинен тестовий випадок поширити AndroidTestCase або потрібно розширити ActivityInstrumentationTestCase2?
Дуг Рей

3
@DougRay У мене була така ж проблема - якщо ви продовжите InstrumentationTestCase, тоді буде знайдено runTestOnUiThread.
Світильник

25

Спосіб вирішити це - запустити будь-який код, який викликає AsyncTask у 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());
}

За замовчуванням junit запускає тести в окрему нитку, ніж основний інтерфейс програми. Документація AsyncTask говорить про те, що екземпляр завдання та виклик Execute () повинні знаходитись у головному потоці інтерфейсу; це тому, що AsyncTask залежить від основного потоку Looperта MessageQueueдля того, щоб його внутрішній обробник працював належним чином.

ПРИМІТКА:

Раніше я рекомендував використовувати @UiThreadTestяк декоратор тестового методу, щоб змусити тест запускатись на основній темі, але це не зовсім правильно для тестування AsyncTask, оскільки, поки ваш тестовий метод працює на головній нитці, повідомлення не обробляються основна черга повідомлень - включаючи повідомлення, які AsyncTask надсилає про свій хід, через що ваш тест зависає.


Це мене врятувало ... хоча мені довелося викликати "runTestOnUiThread" з ще однієї теми, інакше я отримаю "Цей метод не можна викликати з основної нитки програми"
Matthieu

@Matthieu Ви використовували runTestOnUiThread()метод тестування з @UiThreadTestдекоратором? Це не вийде. Якщо методу тестування немає @UiThreadTest, він за замовчуванням повинен запускатися на власному не основному потоці.
Олексій Прецлав

1
Ця відповідь є чистою перлиною. Слід переробити, щоб наголосити на оновленні, якщо ви дійсно хочете зберегти початкову відповідь, викладіть це як основне пояснення та загальну проблему.
Snicolas

1
Документація констатує метод Deprecated in API level 24 developer.android.com/reference/android/test/…
Айварас

1
Застаріле, використовуйте InstrumentationRegistry.getInstrumentation().runOnMainSync()замість цього!
Marco7757

5

Якщо ви не заперечуєте проти виконання AsyncTask в потоці виклику (має бути нормальним у випадку тестування блоку), ви можете використовувати Виконавця в поточній темі, як описано в https://stackoverflow.com/a/6583868/1266123

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

А потім ви запускаєте AsyncTask у вашому тесті одиниці, як це

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

Це працює лише для HoneyComb і вище.


це повинно подорожчати
StefanTo

5

Я написав достатньо об'єднань для Android і просто хочу поділитися, як це зробити.

По-перше, ось клас помічників, який відповідає за очікування та звільнення офіціанта. Нічого особливого:

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

Далі давайте створимо інтерфейс з одним методом, який слід викликати, AsyncTaskколи робота закінчена. Впевнені, що ми також хочемо перевірити наші результати:

TestTaskItf

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

Далі давайте створимо кістяк нашого завдання, який ми перевіримо:

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

Нарешті - наш об’єднаний клас:

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

Це все.

Сподіваюся, це комусь допоможе.


4

Це можна використовувати, якщо ви хочете перевірити результат від doInBackgroundметоду. Перевизначте onPostExecuteметод і виконайте там тести. Дочекатися завершення роботи AsyncTask для використання CountDownLatch. У latch.await()чекаю , поки зворотний відлік часу проходить від 1 (який встановлюється під час ініціалізації) до 0 (що робиться з допомогою 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();
    }

-1

Більшість цих рішень потребують написання великого коду для кожного тесту або для зміни структури вашого класу. Що мені дуже важко використовувати, якщо у вас є багато тестуваних ситуацій або багато AsyncTasks у вашому проекті.

Існує бібліотека, яка полегшує процес тестування AsyncTask. Приклад:

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

В основному, він запускає ваш AsyncTask і перевіряє результат, який він повертає після виклику postComplete().

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.