การทดสอบ Android AsyncTask กับ Android Test Framework


97

ฉันมีตัวอย่างการใช้งาน AsyncTask ที่ง่ายมากและมีปัญหาในการทดสอบโดยใช้ Android JUnit framework

ใช้งานได้ดีเมื่อฉันสร้างอินสแตนซ์และเรียกใช้งานในแอปพลิเคชันปกติ อย่างไรก็ตามเมื่อดำเนินการจากคลาสเฟรมเวิร์กการทดสอบ Android ใด ๆ (เช่น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เป็นกิจกรรมที่ง่ายมากที่ให้ UI สำหรับการทดสอบ 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 ทำสิ่งนี้และดูเหมือนว่างานเบื้องหลังจะไม่ถูกเรียกและ singnal รอตลอดไป :(
Ixx

@Ixx คุณไม่โทรtask.execute(Param...)มาก่อนawait()และใส่countDown()ในonPostExecute(Result)? (ดูstackoverflow.com/a/5722193/253468 ) ยัง @PeterAjtai, Service.doSomethingเป็นสาย async task.executeเช่น
TWiStErRob

ช่างเป็นทางออกที่น่ารักและเรียบง่าย
Maciej Beimcik

Service.doSomething()เป็นที่ที่คุณควรเปลี่ยนการโทรงาน service / async ของคุณ อย่าลืมเรียกsignal.countDown()ใช้วิธีการใด ๆ ที่คุณต้องใช้มิฉะนั้นการทดสอบของคุณจะติดขัด
Victor

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
ขอบคุณสำหรับการเขียนตัวอย่างที่สมบูรณ์ ... ฉันมีปัญหาเล็กน้อยมากมายในการใช้สิ่งนี้
Peter Ajtai

เพียง 1 ปีต่อมาคุณช่วยฉันไว้ ขอบคุณ Billy Brackeen!
MaTT

8
หากคุณต้องการให้การหมดเวลานับเป็นความล้มเหลวในการทดสอบคุณสามารถทำได้:assertTrue(signal.await(...));
Jarett Millard

4
เฮ้ Billy ฉันได้ลองใช้งานนี้แล้ว แต่ไม่พบ runTestOnUiThread กรณีทดสอบควรขยาย AndroidTestCase หรือไม่หรือต้องขยาย ActivityInstrumentationTestCase2
Doug Ray

3
@DougRay ฉันมีปัญหาเดียวกัน - ถ้าคุณขยาย InstrumentationTestCase แล้วจะพบ runTestOnUiThread
Luminaire

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 จะรันการทดสอบในเธรดที่แยกจาก UI ของแอปพลิเคชันหลัก เอกสารของ AsyncTask ระบุว่าอินสแตนซ์งานและการเรียกเพื่อดำเนินการ () ต้องอยู่บนเธรด UI หลัก เนื่องจาก AsyncTask ขึ้นอยู่กับเธรดหลักLooperและMessageQueueเพื่อให้ตัวจัดการภายในทำงานได้อย่างถูกต้อง

บันทึก:

ก่อนหน้านี้ฉันแนะนำให้ใช้@UiThreadTestเป็นมัณฑนากรในวิธีการทดสอบเพื่อบังคับให้การทดสอบทำงานบนเธรดหลัก แต่นี่ไม่เหมาะสำหรับการทดสอบ AsyncTask เนื่องจากในขณะที่วิธีการทดสอบของคุณกำลังทำงานบนเธรดหลักจะไม่มีการประมวลผลข้อความบน MessageQueue หลัก - รวมถึงข้อความที่ AsyncTask ส่งเกี่ยวกับความคืบหน้าทำให้การทดสอบของคุณหยุดทำงาน


ที่ช่วยฉัน ... แม้ว่าฉันจะต้องเรียก "runTestOnUiThread" จากเธรดอื่น แต่ฉันจะได้รับ "วิธีนี้ไม่สามารถเรียกจากเธรดแอปพลิเคชันหลักได้"
Matthieu

@ Matthieu คุณใช้runTestOnUiThread()วิธีทดสอบกับ@UiThreadTestมัณฑนากรหรือไม่? ไม่ได้ผล หากไม่มีวิธีการทดสอบ@UiThreadTestควรรันบนเธรดที่ไม่ใช่เธรดหลักของตนเองตามค่าเริ่มต้น
Alex Pretzlav

1
คำตอบนั้นคืออัญมณีที่บริสุทธิ์ ควรปรับปรุงใหม่เพื่อเน้นการอัปเดตหากคุณต้องการเก็บคำตอบเริ่มต้นไว้เป็นคำอธิบายพื้นหลังและข้อผิดพลาดทั่วไป
Snicolas

1
Documentation States method Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras

1
เลิกใช้แล้วใช้InstrumentationRegistry.getInstrumentation().runOnMainSync()แทน!
Marco7757

5

หากคุณไม่สนใจที่จะเรียกใช้ AsyncTask ในเธรดผู้โทร (ควรใช้ได้ดีในกรณีของการทดสอบหน่วย) คุณสามารถใช้ Executor ในเธรดปัจจุบันได้ตามที่อธิบายไว้ใน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 ขึ้นไป


สิ่งนี้ควรขึ้นไป
Stefan ถึง

5

ฉันเขียน unitests สำหรับ 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()ถูกเรียก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.