使用Android测试框架测试Android AsyncTask

101
我有一个非常简单的AsyncTask实现示例,但在使用Android JUnit框架进行测试时出现了问题。
当我在普通应用程序中实例化和执行它时,它运行得很好。但是,当它从任何一个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 是一个非常简单的活动,提供了用于测试 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());
}

}

这段代码一切都运行良好,除了一个问题:当在 Android 测试框架中执行时,AsyncTask 不会调用其通知方法。有什么想法吗?

9个回答

128
我在实现一些单元测试时遇到了类似的问题。我需要测试与 Executors 协作的某些服务,并且需要将我的服务回调与 ApplicationTestCase 类中的测试方法同步。通常情况下,测试方法本身在回调被访问之前就已经完成了,因此通过回调发送的数据将无法进行测试。尝试应用 @UiThreadTest,但仍然没有解决问题。
我找到了以下方法,它起作用并且我仍在使用它。我只需使用 CountDownLatch 信号对象来实现等待-通知(您可以使用 synchronized(lock){... 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
12
我正在测试一个AsynchTask。我做了这个,但是后台任务似乎永远没有被调用,信号一直等待 :( - User
@Ixx,你是否在await()之前调用了task.execute(Param...) 并将countDown()放在onPostExecute(Result)中?(请参见https://dev59.com/N3E95IYBdhLWcg3watM3#5722193)另外 @PeterAjtai,Service.doSomethingtask.execute一样是一个异步调用。 - TWiStErRob
多么可爱而简单的解决方案。 - Maciej Beimcik
Service.doSomething() 是你应该替换为你的服务/异步任务调用的地方。确保在任何需要实现的方法上调用 signal.countDown(),否则你的测试将会卡住。 - Victor Oliveira

95

我发现了很多类似的答案,但是它们都没有正确地将所有部分组合在一起。因此,这是一个正确的实现方法,适用于在JUnit测试用例中使用android.os.AsyncTask。

 /**
 * 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
8
如果您想让超时视为测试失败,可以执行以下操作:assertTrue(signal.await(...)); - Jarett Millard
三年多过去了,你又拯救了一个测试爱好者.. =).. 谢谢。 - Renan Franca
4
嘿,Billy,我尝试了这个实现但是找不到"runTestOnUiThread"。测试用例应该扩展AndroidTestCase还是需要扩展ActivityInstrumentationTestCase2? - Doug Ray
3
@DougRay 我也遇到了同样的问题 - 如果您扩展 InstrumentationTestCase ,那么 runTestOnUiThread 将被找到。 - Luminaire
显示剩余3条评论

25

处理这个问题的方法是在 runTestOnUiThread() 中运行调用 AsyncTask 的任何代码:

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的文档说明任务实例和execute()调用必须在主UI线程上执行;这是因为AsyncTask依赖于主线程的LooperMessageQueue以使其内部处理程序正常工作。

注意:

之前我建议在测试方法上使用@UiThreadTest作为装饰器来强制测试在主线程上运行,但这对于测试AsyncTask并不完全正确,因为当您的测试方法在主线程上运行时,没有任何关于AsyncTask发送的进度的消息在主MessageQueue上被处理,从而导致测试挂起。


1
那个答案真是一块宝石。如果你真的想保留最初的回答,应该重新强调更新,将其作为一些背景说明和常见陷阱。 - Snicolas
@Snicolas,说得好。我已经重写了我的答案,强调了“更新”,并且摆脱了旧的错误代码。 - Alex Pretzlav
asyncTask.get() 对我来说似乎是更好的方法。不再需要使用CountDownLatch使代码变得混乱。我还尝试在测试中创建一个新的asyncTask对象,然后直接在runTestOnUiThread中调用execute()。看起来它仍然可以正常工作!感谢这个精彩的答案! - tanghao
1
文档说明方法在API级别24中已弃用 https://developer.android.com/reference/android/test/InstrumentationTestCase.html#runTestOnUiThread(java.lang.Runnable) - Ivar
1
已弃用,请改用InstrumentationRegistry.getInstrumentation().runOnMainSync() - Marco7757
显示剩余4条评论

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

希望这对某些人有所帮助。

就是这样了。


5

如果您不介意在调用者线程中执行AsyncTask(在单元测试的情况下应该没问题),则可以像https://dev59.com/6Ww15IYBdhLWcg3wfbzi#6583868中描述的那样在当前线程中使用Executor。

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

然后在单元测试中运行AsyncTask的方法如下:
myAsyncTask.executeOnExecutor(new CurrentThreadExecutor(), testParam);

这仅适用于HoneyComb及更高版本。


这应该上升 - StefanTo

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

0

使用join怎么样?

fun myTest() = runBlocking {
    CoroutineScope(Dispatchers.IO).launch {
        // test something here
    }.join()
}

0

使用这个简单的解决方案

runBlocking{
   //Your code here
}

-1

大多数解决方案需要编写大量代码来进行每个测试或更改类结构。如果您有许多测试情况或项目中有许多异步任务,我发现这非常难以使用。

有一个可以简化测试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()被调用后返回的结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接