Pengujian Android AsyncTask dengan Android Test Framework

97

Saya memiliki contoh implementasi AsyncTask yang sangat sederhana dan saya mengalami masalah dalam mengujinya menggunakan kerangka kerja Android JUnit.

Ini berfungsi dengan baik ketika saya memberi contoh dan menjalankannya dalam aplikasi normal. Namun ketika dijalankan dari salah satu kelas framework Pengujian Android (yaitu AndroidTestCase , ActivityUnitTestCase , ActivityInstrumentationTestCase2 , dll.) Ia berperilaku aneh:

  • Ini menjalankan doInBackground()metode dengan benar
  • Namun tidak memanggil salah metode pemberitahuan nya ( onPostExecute(), onProgressUpdate(), dll) - hanya diam-diam mengabaikan mereka tanpa menunjukkan kesalahan.

Ini adalah contoh AsyncTask yang sangat sederhana:

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

}

Ini adalah kasus uji. Di sini AsyncTaskDemoActivity adalah Aktivitas yang sangat sederhana yang menyediakan UI untuk menguji AsyncTask dalam mode:

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

}

Semua kode ini berfungsi dengan baik, kecuali fakta bahwa AsyncTask tidak memanggil metode pemberitahuannya ketika dijalankan oleh dalam Android Testing Framework. Ada ide?

Vladimir Kroz
sumber

Jawaban:

125

Saya menemui masalah serupa saat mengimplementasikan beberapa unit-test. Saya harus menguji beberapa layanan yang bekerja dengan Executors, dan saya harus memiliki service callback yang disinkronkan dengan metode pengujian dari kelas ApplicationTestCase saya. Biasanya metode pengujian itu sendiri selesai sebelum callback akan diakses, sehingga data yang dikirim melalui callback tidak akan diuji. Mencoba menerapkan payudara @UiThreadTest masih tidak berhasil.

Saya menemukan metode berikut, yang berhasil, dan saya masih menggunakannya. Saya hanya menggunakan objek sinyal CountDownLatch untuk mengimplementasikan tunggu-notify (Anda dapat menggunakan sinkronisasi (kunci) {... lock.notify ();}, namun ini menghasilkan mekanisme kode jelek).

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
sumber
1
Apa Service.doSomething()?
Peter Ajtai
11
Saya sedang menguji AsynchTask. Melakukan ini, dan, yah, tugas latar belakang sepertinya tidak pernah dipanggil dan tunggal menunggu selamanya :(
Ixx
@Ixx, apakah Anda menelepon task.execute(Param...)sebelum await()dan menempatkan countDown()di onPostExecute(Result)? (lihat stackoverflow.com/a/5722193/253468 ) Juga @PeterAjtai, Service.doSomethingadalah panggilan asinkron task.execute.
TWiStErRob
solusi yang indah dan sederhana.
Maciej Beimcik
Service.doSomething()adalah tempat Anda harus mengganti panggilan tugas layanan / asinkron Anda. Pastikan untuk memanggil signal.countDown()metode apa pun yang perlu Anda terapkan atau pengujian Anda akan macet.
Victor R. Oliveira
94

Saya menemukan banyak jawaban yang dekat tetapi tidak satupun dari mereka menyatukan semua bagian dengan benar. Jadi ini adalah salah satu implementasi yang benar saat menggunakan android.os.AsyncTask dalam kasus pengujian JUnit Anda.

 /**
 * 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
sumber
1
terima kasih telah menulis contoh lengkap ... Saya mengalami banyak masalah kecil dalam menerapkan ini.
Peter Ajtai
Lebih dari 1 tahun kemudian, dan Anda menyelamatkan saya. Terima kasih Billy Brackeen!
MaTT
8
Jika Anda ingin waktu tunggu dihitung sebagai kegagalan pengujian, Anda dapat melakukan:assertTrue(signal.await(...));
Jarett Millard
4
Hei Billy, saya telah mencoba penerapan ini tetapi runTestOnUiThread tidak ditemukan. Haruskah kasus uji memperluas AndroidTestCase atau apakah perlu memperluas ActivityInstrumentationTestCase2?
Doug Ray
3
@DougRay Saya memiliki masalah yang sama- jika Anda memperpanjang InstrumentationTestCase maka runTestOnUiThread akan ditemukan.
Luminer
25

Cara untuk mengatasinya adalah dengan menjalankan kode apa pun yang memanggil AsyncTask di 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());
}

Secara default, junit menjalankan pengujian di thread terpisah dari UI aplikasi utama. Dokumentasi AsyncTask mengatakan bahwa instance tugas dan panggilan untuk mengeksekusi () harus ada di thread UI utama; ini karena AsyncTask bergantung pada utas utama Looperdan MessageQueuepenangan internalnya agar berfungsi dengan benar.

CATATAN:

Saya sebelumnya merekomendasikan penggunaan @UiThreadTestsebagai dekorator pada metode pengujian untuk memaksa pengujian berjalan di utas utama, tetapi ini kurang tepat untuk menguji AsyncTask karena sementara metode pengujian Anda berjalan di utas utama, tidak ada pesan yang diproses di utama MessageQueue - termasuk pesan yang dikirim AsyncTask tentang kemajuannya, menyebabkan pengujian Anda macet.

Alex Pretzlav
sumber
Itu menyelamatkan saya ... meskipun saya harus memanggil "runTestOnUiThread" dari utas lain jika tidak, saya akan mendapatkan "Metode ini tidak dapat dipanggil dari utas aplikasi utama"
Matthieu
@ Matthieu Apakah Anda menggunakan runTestOnUiThread()metode uji dengan @UiThreadTestdekorator? Itu tidak akan berhasil. Jika metode pengujian tidak memilikinya @UiThreadTest, ia harus berjalan di thread non-utamanya sendiri secara default.
Alex Pretzlav
1
Jawaban itu adalah permata murni. Ini harus dikerjakan ulang untuk menekankan pada pembaruan, jika Anda benar-benar ingin menyimpan jawaban awal, letakkan sebagai penjelasan latar belakang dan perangkap umum.
Snicolas
1
Dokumentasi menyatakan metode Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras
1
Tidak digunakan lagi, gunakan InstrumentationRegistry.getInstrumentation().runOnMainSync()saja!
Marco7757
5

Jika Anda tidak keberatan menjalankan AsyncTask di thread pemanggil (seharusnya baik-baik saja jika dilakukan pengujian Unit), Anda dapat menggunakan Executor di thread saat ini seperti yang dijelaskan di https://stackoverflow.com/a/6583868/1266123

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

Dan kemudian Anda menjalankan AsyncTask Anda dalam pengujian unit Anda seperti ini

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

Ini hanya berfungsi untuk HoneyComb dan yang lebih tinggi.

robUx4
sumber
ini harus naik
Stefan Sampai
5

Saya menulis cukup unitests untuk Android dan hanya ingin berbagi cara melakukannya.

Pertama, inilah kelas helper yang bertanggung jawab untuk menunggu dan melepaskan waiter. Tidak ada yang spesial:

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

Selanjutnya, mari buat antarmuka dengan satu metode yang harus dipanggil dari AsyncTasksaat pekerjaan selesai. Tentu kami juga ingin menguji hasil kami:

TestTaskItf

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

Selanjutnya mari kita buat beberapa kerangka Tugas kita yang akan kita uji:

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

Akhirnya - kelas unitest kami:

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

Itu saja.

Semoga bisa membantu seseorang.

Maxim Shoustin
sumber
4

Ini dapat digunakan jika Anda ingin menguji hasil dari doInBackgroundmetode tersebut. Ganti onPostExecutemetode dan lakukan pengujian di sana. Untuk menunggu AsyncTask selesai, gunakan CountDownLatch. The latch.await()menunggu sd berjalan mundur dari 1 (yang diatur selama inisialisasi) ke 0 (yang dilakukan oleh countdown()metode).

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

Sebagian besar solusi tersebut memerlukan banyak kode yang harus ditulis untuk setiap pengujian atau untuk mengubah struktur kelas Anda. Yang menurut saya sangat sulit digunakan jika Anda memiliki banyak situasi yang sedang diuji atau banyak AsyncTasks pada proyek Anda.

Ada perpustakaan yang memudahkan proses pengujian AsyncTask. Contoh:

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

Pada dasarnya, ini menjalankan AsyncTask Anda dan menguji hasil yang dikembalikannya setelah postComplete()dipanggil.

Leonardo Soares e Silva
sumber