Thử nghiệm Android AsyncTask với Khung thử nghiệm Android


97

Tôi có một ví dụ triển khai AsyncTask rất đơn giản và đang gặp sự cố khi kiểm tra nó bằng khung Android JUnit.

Nó hoạt động tốt khi tôi khởi tạo và thực thi nó trong ứng dụng bình thường. Tuy nhiên, khi nó được thực thi từ bất kỳ lớp khung kiểm tra Android nào (ví dụ: AndroidTestCase , ActivityUnitTestCase , ActivityIusalmentationTestCase2 , v.v.) nó hoạt động kỳ lạ:

  • Nó thực thi doInBackground()phương pháp một cách chính xác
  • Tuy nhiên nó không gọi bất kỳ phương pháp thông báo của nó ( onPostExecute(), onProgressUpdate(), vv) - chỉ âm thầm bỏ qua chúng mà không hiển thị bất kỳ lỗi nào.

Đây là ví dụ AsyncTask rất đơn giản:

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

}

Đây là một trường hợp thử nghiệm. Ở đây AsyncTaskDemoActivity là một Hoạt động rất đơn giản cung cấp giao diện người dùng để kiểm tra AsyncTask ở chế độ:

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

}

Tất cả mã này đều hoạt động tốt, ngoại trừ thực tế là AsyncTask không gọi các phương thức thông báo của nó khi được thực thi bởi trong Khung thử nghiệm Android. Bất kỳ ý tưởng?

Câu trả lời:


125

Tôi đã gặp sự cố tương tự khi thực hiện một số bài kiểm tra đơn vị. Tôi đã phải kiểm tra một số dịch vụ hoạt động với Người thực thi và tôi cần đồng bộ hóa các lệnh gọi lại dịch vụ của mình với các phương pháp kiểm tra từ các lớp ApplicationTestCase của tôi. Thông thường, bản thân phương thức kiểm tra đã hoàn thành trước khi lệnh gọi lại được truy cập, vì vậy dữ liệu được gửi qua lệnh gọi lại sẽ không được kiểm tra. Đã thử áp dụng @UiThreadTest phá sản vẫn không hoạt động.

Tôi đã tìm thấy phương pháp sau đây, phương pháp này hiệu quả và tôi vẫn sử dụng nó. Tôi chỉ đơn giản sử dụng các đối tượng tín hiệu CountDownLatch để triển khai cơ chế chờ thông báo (bạn có thể sử dụng cơ chế đồng bộ hóa (khóa) {... lock.notify ();}, tuy nhiên điều này dẫn đến mã xấu).

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()gì?
Peter Ajtai

11
Tôi đang thử nghiệm AsynchTask. Đã làm được điều này, và, tốt, nhiệm vụ nền dường như không bao giờ được gọi và chỉ đợi mãi mãi :(
Ixx

@Ixx, bạn đã gọi task.execute(Param...)trước await()và đặt countDown()vào onPostExecute(Result)? (xem stackoverflow.com/a/5722193/253468 ) Ngoài ra @PeterAjtai, Service.doSomethinglà một lệnh gọi không đồng bộ như task.execute.
TWiStErRob

thật là một giải pháp dễ thương và đơn giản.
Maciej Beimcik

Service.doSomething()là nơi bạn nên thay thế lệnh gọi tác vụ dịch vụ / không đồng bộ của mình. Đảm bảo gọi signal.countDown()bất kỳ phương pháp nào bạn cần triển khai nếu không bài kiểm tra của bạn sẽ gặp khó khăn.
Victor R. Oliveira

94

Tôi đã tìm thấy rất nhiều câu trả lời gần giống nhau nhưng không có câu trả lời nào trong số họ đặt tất cả các phần lại với nhau một cách chính xác. Vì vậy, đây là một cách triển khai đúng khi sử dụng android.os.AsyncTask trong các trường hợp kiểm tra JUnit của bạn.

 /**
 * 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
cảm ơn vì đã viết ra một ví dụ hoàn chỉnh ... Tôi đã gặp rất nhiều vấn đề nhỏ khi thực hiện điều này.
Peter Ajtai

Chỉ hơn 1 năm sau, và bạn đã cứu tôi. Cảm ơn Billy Brackeen!
MaTT

8
Nếu bạn muốn có một thời gian chờ để tính là một thử nghiệm thất bại, bạn có thể làm:assertTrue(signal.await(...));
Jarett Millard

4
Này Billy Tôi đã thử triển khai này nhưng không tìm thấy runTestOnUiThread. Trường hợp thử nghiệm có nên mở rộng AndroidTestCase hay nó cần phải mở rộng ActivityInticmentationTestCase2?
Doug Ray

3
@DougRay Tôi cũng gặp sự cố tương tự- nếu bạn mở rộng InstrumentationTestCase thì sẽ tìm thấy runTestOnUiThread.
Luminaire

25

Cách để giải quyết vấn đề này là chạy bất kỳ mã nào gọi AsyncTask trong 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());
}

Theo mặc định, junit chạy các bài kiểm tra trong một chuỗi riêng biệt với giao diện người dùng ứng dụng chính. Tài liệu của AsyncTask nói rằng phiên bản nhiệm vụ và lệnh gọi thực thi () phải nằm trên chuỗi giao diện người dùng chính; điều này là do AsyncTask phụ thuộc vào luồng chính LooperMessageQueuetrình xử lý nội bộ của nó hoạt động bình thường.

GHI CHÚ:

Trước đây tôi đã khuyến nghị sử dụng @UiThreadTestlàm trình trang trí trên phương pháp thử nghiệm để buộc thử nghiệm chạy trên luồng chính, nhưng điều này không hoàn toàn phù hợp để thử nghiệm AsyncTask vì trong khi phương pháp thử nghiệm của bạn đang chạy trên luồng chính, không có thông báo nào được xử lý trên chính MessageQueue - bao gồm các thông báo mà AsyncTask gửi về tiến trình của nó, khiến bài kiểm tra của bạn bị treo.


Điều đó đã cứu tôi ... mặc dù tôi phải gọi "runTestOnUiThread" từ một chuỗi khác, nếu không, tôi sẽ nhận được "Không thể gọi phương thức này từ chuỗi ứng dụng chính"
Matthieu

@Matthieu Bạn có đang sử dụng runTestOnUiThread()phương pháp thử nghiệm với trình @UiThreadTesttrang trí không? Điều đó sẽ không hoạt động. Nếu một phương thức kiểm tra không có @UiThreadTest, nó sẽ chạy trên luồng không phải của chính nó theo mặc định.
Alex Pretzlav

1
Câu trả lời đó là viên ngọc thuần khiết. Nó nên được làm lại để nhấn mạnh vào bản cập nhật, nếu bạn thực sự muốn giữ câu trả lời ban đầu, hãy đặt nó như một số giải thích cơ bản và một cạm bẫy phổ biến.
Snicolas

1
Tài liệu cho biết phương pháp Deprecated in API level 24 developer.android.com/reference/android/test/…
Aivaras

1
Không được dùng nữa, hãy sử dụng InstrumentationRegistry.getInstrumentation().runOnMainSync()thay thế!
Marco7757

5

Nếu bạn không phiền khi thực thi AsyncTask trong chuỗi người gọi (sẽ ổn trong trường hợp Unit testing), bạn có thể sử dụng Executor trong chuỗi hiện tại như được mô tả trong https://stackoverflow.com/a/6583868/1266123

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

Và sau đó bạn chạy AsyncTask của mình trong bài kiểm tra đơn vị của bạn như thế này

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

Điều này chỉ hoạt động cho HoneyComb trở lên.


điều này sẽ tăng lên
StefanTo

5

Tôi đã viết đủ đơn vị đo cho Android và chỉ muốn chia sẻ cách làm điều đó.

Trước hết, đây là lớp helper chịu trách nhiệm chờ và giải phóng bồi bàn. Không có gì đặc biệt:

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

Tiếp theo, hãy tạo giao diện với một phương thức sẽ được gọi AsyncTaskkhi công việc hoàn thành. Chắc chắn chúng tôi cũng muốn kiểm tra kết quả của mình:

TestTaskItf

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

Tiếp theo, hãy tạo một số khung của Nhiệm vụ của chúng ta mà chúng ta sẽ kiểm tra:

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

Cuối cùng - lớp học đơn nhất của chúng tôi:

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

Đó là tất cả.

Hy vọng nó sẽ giúp ích cho ai đó.


4

Điều này có thể được sử dụng nếu bạn muốn kiểm tra kết quả từ doInBackgroundphương pháp. Ghi đè onPostExecutephương pháp và thực hiện các thử nghiệm ở đó. Để đợi AsyncTask hoàn thành, hãy sử dụng CountDownLatch. Các latch.await()đợi đến khi chạy đếm ngược từ 1 (được thiết lập trong quá trình khởi) 0 (được thực hiện bởi các countdown()phương pháp).

@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

Hầu hết các giải pháp đó đều yêu cầu viết rất nhiều mã cho mọi bài kiểm tra hoặc để thay đổi cấu trúc lớp của bạn. Tôi thấy rất khó sử dụng nếu bạn có nhiều tình huống đang thử nghiệm hoặc nhiều AsyncTasks trong dự án của bạn.

Có một thư viện giúp giảm bớt quá trình kiểm tra AsyncTask. Thí dụ:

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

Về cơ bản, nó chạy AsyncTask của bạn và kiểm tra kết quả mà nó trả về sau khi postComplete()được gọi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.