To main content
 
Виктор Мацкевич

Асинхронные задачи в приложении и Android Instrumentation Test.
Доступные решения

В настоящее время практически во всех приложениях присутствуют асинхронные задачи. Яркий пример тому - запросы к серверу.

Рассмотрим следующий случай. Модернизируем наш калькулятор из прошлой статьи. Представим, что по нажатию на кнопку «Calculate» выполняется запрос, ответ которого будет являться результатом вычисления. Для этого переделаем обработчик нажатия кнопки «Calculate».

Обвернём метод calculate() классa MainActivity.java в AsyncTask:
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
try {
Thread.sleep(4000);
runOnUiThread(new Runnable() {
@Override
public void run() {
calculate();
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
view raw 10.java hosted with ❤ by GitHub
AsyncTask мы используем для имитации обычного запроса на сервер.

После чего напишем новый тест для проверки функции calculate().
public void testCalculate_2plus2_4() {
assertNotNull(mMainActivity);
final Button addButton = mMainActivity.getAddButton();
final EditText inputDataEditText = mMainActivity.getInputDataEditText();
mMainActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
inputDataEditText.setText(String.valueOf(2));
addButton.performClick();
inputDataEditText.setText(String.valueOf(2));
}
});
getInstrumentation().waitForIdleSync();
assertEquals(inputDataEditText.getText().toString(), "2");
assertEquals(mMainActivity.getTextResult(), "2.0 +");
assertTrue(mMainActivity.mFirstValue == 2);
assertTrue(mMainActivity.mSecondValue == 0);
final Button calculateButton = mMainActivity.getCalculateButton();
mMainActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
calculateButton.performClick();
}
});
getInstrumentation().waitForIdleSync();
assertEquals(mMainActivity.getTextResult(), "2.0 + 2.0 = 4.0");
}
view raw 11.java hosted with ❤ by GitHub
И снова запустим наш тест.
Тест не пройден. Почему? На картинке ниже наглядно продемонстрировано, что пошло не так:
Напомним, что метод calculate() теперь вызывается в методе doInBackground() класса AsyncTask. Данный метод вызывается не в основном потоке, а инструмент getInstrumentation()
.waitForIdleSync()
ждёт пока очередь у основного потока будет не пустой. Вследствие чего возникает проблема в том, что проверка результата происходит раньше, чем все потоки синхронизируются. Что нас, конечно же, не устраивает.

В связи с этим возникает потребность осуществлять искусственную синхронизацию для корректного прохождения теста.

Сделать это можно следующими способами:


  • getInstrumentation()
    .waitForIdleSync()
  • Synchronized
waitForIdleSync()
В прошлой статье шла речь об этой функции. Кратко напомню: она ждёт, пока все UI потоки завершат свою работу. Как же нам можно применить её, если созданный нами поток не является UI потоком?

Всё очень просто. Нам необходимо добавить анимированный объект, анимация которого завершится после выполнения функции calculate(). Самый очевидный вариант - добавить в наш layout ProgressBar.

Немного перестроим структуру разметки. Ниже представлена новая разметка:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.bytepace.test.calculator.MainActivity">
<LinearLayout
android:id="@+id/ll_input_data_panel"
android:gravity="center"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:inputType="number"
android:id="@+id/et_input_data"
android:layout_weight="0.7"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:text="@string/calculate"
android:id="@+id/btn_calculate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.3"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_operations_panel"
android:layout_below="@+id/ll_input_data_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:text="@string/operation_add"
android:id="@+id/btn_add"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:text="@string/operation_subtract"
android:id="@+id/btn_subtract"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:text="@string/operation_divide"
android:id="@+id/btn_divide"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_multiply"
android:text="@string/operation_multiply"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_clean"
android:text="@string/operation_ce"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:layout_below="@+id/ll_operations_panel"
android:id="@+id/tv_result"
android:gravity="center"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:clickable="true"
android:id="@+id/ll_progress_bar_panel"
android:alpha="0.3"
android:background="@android:color/black"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp" />
</LinearLayout>
</RelativeLayout>
view raw 12.xml hosted with ❤ by GitHub
А также немного модернизируем наш код. Теперь он выглядит вот так.

После чего прогоняем заново тест. Отлично, тест пройден!
Получилась следующая ситуация. Когда мы отобразили ProgressBar, включилась его анимация. Все действия, связанные с представлением, являются UI потоком. Собственно поэтому и функция getInstrumentation()
.waitForIdleSync()
находится в ожидании.

Но у такого метода есть огромный минус. Иногда необходимо убрать анимацию на тестируемом устройстве для проверки. Если мы выключим анимацию, то, собственно, ожидания не будет. Именно поэтому мы сейчас рассмотрим второй случай, связанный с синхронизацией потоков.
Synchronized
Суть приёма заключается в следующем: мы создаём объект и, применив конструкцию synchronized(Object) {}, останавливаем тест, пока наш объект не вызовет функцию notify(). Данную функцию он вызовет, когда сработает метод коллбека.

Для реализации данного приёма необходимо выполнить следующие шаги:


  1. Написать интерфейс для обратной связи
  2. Создать экземпляр этого интерфейса в тесте и передать его активити, переопределить его и установить обращение к его методам
  3. Добавить точку синхронизации в тесте
Начнём с написания интерфейса. Выглядеть он будет следующим образом:
public interface MainActivityCallBack {
void calculateIsDone();
}
view raw 13.java hosted with ❤ by GitHub
Идея заключается в том, что метод calculateIsDone() вызовется после выполнения расчёта.

В классе MainActivity.java объявляем переменную mMainActivityCallBack, она является экземпляром интерфейса MainActivityCallBack.java, и создаём setter для этой переменной:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
private MainActivityCallBack mMainActivityCallBack;
...
public void setMainActivityCallBack(MainActivityCallBack callBack) {
mMainActivityCallBack = callBack;
}
...
}
view raw 14.java hosted with ❤ by GitHub
Но на этом не всё, нам ещё необходимо добавить вызов метода calculateIsDone(). Для этого переходим в метод calculate() и дописываем после выполнения расчёта следующее:
if(mMainActivityCallBack != null)
mMainActivityCallBack.calculateIsDone();
view raw 15.java hosted with ❤ by GitHub
Перейдем теперь к классу MainActivityTest.java. Добавим следующий фрагмент кода перед вызовом функции calculateButton.performClick():
final Object syncObject = new Object();
mMainActivity.setMainActivityCallBack(new MainActivityCallBack() {
@Override
public void calculateIsDone() {
synchronized (syncObject) {
syncObject.notify();
}
}
});
view raw 16.java hosted with ❤ by GitHub
Здесь мы просто создали объект для синхронизации и изменили callback для activity. В переопределенном методе, вызвав функцию notify(), мы синхронизируем объект syncObject.

Также стоит добавить следующий фрагмент после вызова метода calculateButton.performClick():
synchronized (syncObject) {
syncObject.wait();
}
view raw 17.java hosted with ❤ by GitHub
Это наша точка синхронизации, метод wait() ждет, пока не выполнится метод notify(). Более подробно этот процесс мы можем увидеть на рисунке ниже.
После чего запускаем наш тест и видим, что тест пройден.
Вот таким незамысловатым способом нам удалось решить проблему асинхронных задач.

Вообще, как правило, то, что мы добавляли методы для тестов в основной класс — нехорошо. Обычно создают вспомогательный класс и все дополнительные функции дописывают там. Потому что не стоит мешать вместе методы для основной работы приложения и тестов, лучше разграничивать функционал. Об этом будет сказано в следующей статье в качестве заключительной части.
Спасибо за внимание!