Тестирование Android AsyncTask с помощью Android Test Framework
У меня очень простой пример реализации AsyncTask, и у меня возникла проблема с его тестированием с использованием платформы Android JUnit.
Он отлично работает, когда я создаю экземпляр и выполняю его в обычном приложении.
Однако, когда он выполняется из любого из классов фреймворка 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. Любые идеи?
Ответы
Ответ 1
У меня возникла аналогичная проблема при реализации некоторого модульного теста. Мне пришлось протестировать некоторые службы, которые работали с Executors, и мне нужно было, чтобы мои обратные вызовы службы синхронизировались с методами тестирования из моих классов ApplicationTestCase. Обычно сам метод тестирования заканчивается до того, как обратный вызов будет доступен, поэтому данные, отправленные через обратные вызовы, не будут проверены. Пробовал использовать @UiThreadTest бюст все еще не работал.
Я нашел следующий метод, который работал, и я все еще использую его. Я просто использую объекты сигнала CountDownLatch для реализации wait-notify (вы можете использовать механизм 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
}
Ответ 2
Я нашел много близких ответов, но ни один из них не сошёл правильно. Таким образом, это одна правильная реализация при использовании 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);
}
Ответ 3
Способ справиться с этим - запустить любой код, который вызывает 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, потому что, пока ваш тестовый метод работает в основном потоке никакие сообщения не обрабатываются в основном MessageQueue - включая сообщения, которые AsyncTask отправляет о своем прогрессе, в результате чего ваш тест зависает.
Ответ 4
Если вы не против выполнения AsyncTask в потоке вызывающего абонента (должно быть хорошо в случае модульного тестирования), вы можете использовать Executor в текущем потоке, как описано в fooobar.com/questions/70341/...
public class CurrentThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
И затем вы запустите свой AsyncTask в своем unit test, как этот
myAsyncTask.executeOnExecutor(new CurrentThreadExecutor(), testParam);
Это работает только для HoneyComb и выше.
Ответ 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
}
}
}
Наконец, наш класс unitest:
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)
}
}
Что все.
Надеюсь, это поможет кому-то.
Ответ 6
Это можно использовать, если вы хотите проверить результат по методу 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();
}
Ответ 7
В большинстве этих решений требуется много кода для каждого теста или для изменения структуры вашего класса. Который мне очень сложно использовать, если у вас много тестов или много асинхронных задач в вашем проекте.
Существует библиотека (https://github.com/lsoaresesilva/asynctasktest), которая облегчает процесс тестирования 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.